Builder is a C++ build framework organized around modules.
The usual workflow is:
- Write a C++ module.
- Write the module's
builder.cpp. - Run the module with
./cli <module> [args...].
Builder currently targets a Linux or Linux-compatible POSIX environment. The
default bootstrap makefile expects clang++ (C++23), clang, ar, ln, mkdir, mv,
and rm under /usr/bin, and libdl.
Bootstrap the local Builder CLI:
make -f ws0/m03gagbhst621faiop1rztfkqp_builder_cli/bootstrap.mk bootstrapThis creates the initial Builder CLI and builder plugin used to rebuild the modules that implement Builder itself.
Run existing modules from the current workspace graph, couple of examples:
./cli m03gagbht2l61mj6qitacwbmea_byte_stream
./cli m03gagbhtft23yhjwpp881tfmc_uuidModule that generates a new module with minimal boilerplate:
./cli m03gagbht5685jfnokvj7crv2c_create_module <workspace> hello_moduleThe normal command is:
./cli <module> [args...]<module> is the globally unique module name. Builder discovers that module from the
workspace graph, builds its default CLI if needed, then execs that CLI with
[args...].
Builder uses two environment variables to find source and output locations:
BUILDER_WORKSPACE_ROOT: directory containing the workspace directories; defaults to the current directory.BUILDER_ARTIFACT_ROOT: directory where Builder writes generated files; defaults to<BUILDER_WORKSPACE_ROOT>/artifacts.
Builder sets these variables before running a module CLI, so the CLI and its child processes see the same workspace and artifact locations.
A module is the unit Builder builds and runs. It owns:
- source files;
builder.cpp;- build output;
- public headers;
- linkable library capability;
- default CLI capability.
A module directory is:
<workspace>/<module_name>/
Module names must be globally unique because includes and ./cli <module> refer
to modules by name.
This repository uses generated module names with a durable identity prefix and a human-readable semantic slug:
m<zero-padded-base36-uuidv7>_<semantic_slug>
For example:
m03gagbht2l61mj6qitacwbmea_base36
The whole string is the module identity. The generated prefix keeps module names globally unique across workspaces and repositories. The semantic slug makes the module easier to recognize in includes, dependency lists, commands, and diagnostics, but it is still part of the identity.
Workspaces define which modules may depend on which other modules. Workspaces are
directories named wsN under the workspace root:
ws0/
ws1/
ws2/
...
The numeric suffix is the workspace order from lower-level to higher-level.
The rules are:
- module source may depend on modules in the same workspace or a lower workspace;
builder.cppmay depend only on modules in lower workspaces.
Workspace ws0 is bootstrap-specific. It contains the modules needed to
seed and rebuild Builder itself. For that active Builder bootstrap group, modules
that implement the running Builder can use same-workspace builder dependencies so
the local bootstrap seed can build Builder without a lower bootstrap workspace.
That exception is not yet a general module authoring semantic. Higher-level modules should normally live in later workspaces, and builder dependency cycles are not legal outside the active Builder bootstrap group.
Builder derives dependencies from module-qualified includes:
#include <module_name/header.h>Includes in source files and public headers become module dependencies. Includes
in builder.cpp become builder dependencies. Builder exposes builder
dependencies to builder.cpp as normal C++ include and link inputs.
Include the public header for each module API you reference directly. Do not rely on transitive includes or forward declarations of another module's API to make your code compile.
Dependencies outside these rules fail before build output is produced.
Same-workspace module dependency cycles are handled as strongly connected component groups. The group graph is ordered topologically from dependency to dependent, and every module in a group can publish interfaces before library compilation. Builder dependency cycles are not modeled as module groups.
A build configuration selects library type and phase order. The default phase order is:
source -> interface -> library -> binary
The phases are:
source: make source-side inputs available;interface: publish public headers;library: publish linkable library output;binary: publish executable output, including the default CLI.
builder.cpp includes the phase API and defines one C-callable function per
phase.
#include <m03gagbhsujjf63n0w3r2w4q6h_build_phases/build_phases.h>
extern "C" void phase__source(const m03gagbhsujjf63n0w3r2w4q6h_build_phases::source_phase_t* phase) {
phase->install_source_tree();
}
extern "C" void phase__interface(const m03gagbhsujjf63n0w3r2w4q6h_build_phases::interface_phase_t* phase) {
phase->install_headers_from_source();
}
extern "C" void phase__library(const m03gagbhsujjf63n0w3r2w4q6h_build_phases::library_phase_t* phase) {
}
extern "C" void phase__binary(const m03gagbhsujjf63n0w3r2w4q6h_build_phases::binary_phase_t* phase) {
phase->install_cli({});
}Builder owns graph order, group order, and phase order. A builder only describes local work for the phase it is given.
Each phase has two roots:
build_dir(): private scratch for the current phase;install_dir(): public completed output for later phases.
Generated files and compiler outputs belong under build_dir(). Published
paths are copied into install_dir().
Important phase APIs:
build(path): selects a path from an earlier phaseinstall_dir()for compile/link helpers;install(path): publishes a path into the current phaseinstall_dir()under the same relative path. The path must come from the currentbuild_dir()or an earlier phaseinstall_dir(). Installing two paths to the same relative path is an error.install<T>(): ensures phaseTis installed and returns its result.
build_library(...) and install_cli(...) accept compile defines for the sources
being compiled. install_cli(...) always builds the module's cli.cpp and
publishes it as the default cli artifact. Compile define keys are validated by
m03gagbhsmhr0naw0zpccv4gaq_cxx_toolchain::define_t and should use a
macro-safe module identity prefix when they are module-owned. Compile defines
are private compile inputs; public module headers should not expose private
macros unless the macro is deliberately part of the public API. External host
tools should be validated when the code that uses them runs, not while
installing unrelated phases.
Public includes are module-qualified:
#include <module_name/header.h>For a module named m03gagbht2l61mj6qitacwbmea_base36 with base36.h,
install_interface(...) publishes this include:
#include <m03gagbht2l61mj6qitacwbmea_base36/base36.h>Imported upstream packages sometimes include their own headers without Builder's
module prefix. install_interface_compatibility(...) publishes the extra include
root needed for those upstream self-includes. Module users should still include
through the module name.
Public headers that use include guards should derive the guard from the module name and the module-relative installed header path.
builder.cpp is the module's C++ build file. Its job is to build that module's
own outputs: published headers, libraries, generated files, imported files, and
binaries.
It is regular C++ code. If builder.cpp needs helper code, tools, or libraries,
it includes their module-qualified public headers. Builder then makes those
dependencies available to builder.cpp through normal C++ includes and linking.
Each phase function in builder.cpp describes only the local work for that
phase. For example, it can install headers, compile source files, generate a
file under build_dir(), run an imported tool, or publish the module's default
CLI.
To build a library, replace the empty phase__library function above with one
that compiles a source file from the source phase install_dir() and publishes
the library:
#include <m03gagbhsujjf63n0w3r2w4q6h_build_phases/build_phases.h>
#include <m03gagbhsnusi43zogoacgj2ez_filesystem/filesystem.h>
extern "C" void phase__library(const m03gagbhsujjf63n0w3r2w4q6h_build_phases::library_phase_t* phase) {
const auto sources = phase->install<m03gagbhsujjf63n0w3r2w4q6h_build_phases::source_phase_t>();
const auto library = phase->build_library({
phase->build(sources.root() / m03gagbhsnusi43zogoacgj2ez_filesystem::relative_path_t("base36.cpp") )
}, {});
phase->install_library(library);
}Builder writes generated files under BUILDER_ARTIFACT_ROOT.
Artifacts are grouped by module name, but the exact directory layout is an implementation detail:
<BUILDER_ARTIFACT_ROOT>/<module_name>/
Builder exposes a stable latest view for tooling and debugging:
<BUILDER_ARTIFACT_ROOT>/<module_name>/latest/
latest is a stable path for external tooling, but it is not dependency
versioning or a module authoring API. Module builders should use phase APIs such
as build_dir(), build(path), install(path), and install<T>() instead of
hard-coding artifact paths or inspecting phase install roots directly.
m03gagbhsp2drqq3gkop8pzfrm_workspace_graph: discovers modules, derives dependency edges from includes, validates workspace order, forms module dependency strongly connected component groups, and propagates versions.m03gagbhsujjf63n0w3r2w4q6h_build_phases: defines phase APIs, lazy installation, phase roots, interface publication, library builds, and binary builds.m03gagbhst621faiop1rztfkqp_builder_cli: implements./cli <module> [args...], CLI self-update, target binary installation, and process execution.m03gagbhsmhr0naw0zpccv4gaq_cxx_toolchain: compiles objects, archives static libraries, links shared libraries, links binaries, and injects include/link inputs.m03gagbhsnusi43zogoacgj2ez_filesystem: filesystem path and file operations.m03gagbhsvr0m5w15urj0o291m_process: process execution.m03gagbhsyhlx2pk5sdabbr1sx_signal_handler: signal-aware cleanup guards.m03gagbhsx4j5z28bqkac3dhhh_shared_library: shared library loading.
- Support versioned module dependencies.
- Support composing modules by name through a small LISP-like expression language.
- Support module tests as first-class graph nodes with declared dependencies.
See LICENSE.