ADR-0005: CMake as the Build System for C Components
Context
With C23 chosen as the core language (ADR-0002) and a component protocol
established for workspace integration (ADR-0004), each C component
needs an internal build system that handles configuration, compilation, testing, and installation behind its
protocol-mandated Makefile.
The build system must support:
- C23 compilation with strict warnings and targeted additional flags, as mandated by ADR-0002.
- Sanitizers (AddressSanitizer, UndefinedBehaviorSanitizer, MemorySanitizer) as build options for CI.
- Static analysis integration (clang-tidy, clang static analyzer).
- pkg-config file generation for every public library.
- Dependency discovery through standard mechanisms (pkg-config,
CMAKE_PREFIX_PATH) using the workspaceSYSROOT. - Out-of-tree builds so that source directories remain clean.
Standardizing on a single build system across all C components reduces friction for contributors and enables consistent tooling and conventions.
Decision
All C components will use CMake (minimum version 3.21) as their internal build system, generating Ninja build files by default.
Component structure
Each C component will follow this layout:
component/
├── Makefile # Component protocol shim — delegates to CMake
├── CMakeLists.txt # Root CMake configuration
├── CMakePresets.json # Build presets (dev, release, coverage, sanitizers, CI)
├── cmake/ # CMake modules (compiler, sanitizers, coverage, tools, install, ...)
├── include/ # Public headers
├── src/ # Source files
└── docs/ # Component documentation
The root Makefile is a thin wrapper that maps the component protocol targets to CMake operations:
| Makefile target | CMake operation |
|---|---|
build | cmake --preset $(PRESET) + cmake --build --preset |
install | cmake --install |
test | ctest --preset |
clean | cmake --build --preset --target clean |
distclean | Remove the build directory entirely |
CMAKE_INSTALL_PREFIX is set from the protocol's PREFIX variable and CMAKE_PREFIX_PATH from SYSROOT,
so that find_package() and pkg_check_modules() discover workspace-staged dependencies automatically.
Build presets
Components define CMake presets for common scenarios:
- dev — RelWithDebInfo, tests enabled.
- debug — Debug symbols, tests enabled.
- release — Optimized, no tests.
- coverage — Debug with coverage instrumentation.
- asan — AddressSanitizer + UndefinedBehaviorSanitizer.
- tsan — ThreadSanitizer.
- ci — Full build with tests and documentation.
Build conventions
- C23 as the language standard (
CMAKE_C_STANDARD 23,CMAKE_C_STANDARD_REQUIRED ON). - Strict warnings:
-Wall -Wextra -Wpedantic -Werrorplus targeted warnings for undefined behavior, type safety, format strings, and C-specific pitfalls, with compiler-specific extensions for GCC and Clang. - Sanitizer options exposed as CMake cache variables, enabled in dedicated presets.
- Interprocedural optimization (LTO) enabled when supported.
- Symbol visibility hidden by default for libraries.
- pkg-config files generated and installed to
lib/pkgconfig/for every public library. compile_commands.jsonexported for language server and clang-tidy integration.
Alternatives Considered
Meson. A modern build system popular in the freedesktop ecosystem (Wayland, GStreamer, GTK), with concise declarative syntax and built-in support for sanitizers and pkg-config. However, Meson deliberately limits expressiveness — its restricted scripting, lack of custom build rules, and opinionated constraints become obstacles when a project needs code generation, non-trivial cross-component tooling, or build logic that does not fit Meson's model. CMake's flexibility handles these cases without workarounds or external scripts.
Plain GNU Make. Already used as the component protocol interface and would eliminate an additional dependency. However, writing correct and maintainable Makefiles for non-trivial C projects — dependency tracking, out-of-tree builds, cross-compilation, install rules, pkg-config generation — requires significant boilerplate and expertise. The component protocol provides the Make interface; duplicating the internal build logic in Make gains nothing over delegating to a proper build system.
GNU Autotools (Autoconf / Automake). The traditional choice for portable C projects, with decades of deployment. However, Autotools is complex to set up, slow to configure, and requires maintaining multiple generated files (configure, Makefile.in, config.h.in). Its portability to exotic Unix variants comes at a cost in complexity that is difficult to justify for a new project.
Consequences
- De facto standard. CMake is the most widely adopted build system for C and C++ projects. Contributors are likely to have prior experience with it, and extensive documentation and community support are available.
- Versatility. CMake's scripting capabilities, generator expressions, custom commands, and toolchain files provide the flexibility needed for code generation, plugin system integration, cross-compilation, and non-trivial build logic without resorting to external scripts.
- IDE and tooling support. CMake is natively supported by virtually every C/C++ IDE and editor (CLion,
VS Code, Qt Creator, Emacs, Vim). The
compile_commands.jsonexport enables seamless clang-tidy and language server integration. - Makefile shim. Each component needs a thin Makefile that delegates to CMake. This is a small amount of boilerplate (under 30 lines), stable across components, and will be documented in the developer guide.
- Dependency on CMake and Ninja. Contributors and CI must install CMake (3.21+) and Ninja. Both are widely packaged across distributions.