Skip to main content

ADR-0005: CMake as the Build System for C Components

Accepted

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 workspace SYSROOT.
  • 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 targetCMake operation
buildcmake --preset $(PRESET) + cmake --build --preset
installcmake --install
testctest --preset
cleancmake --build --preset --target clean
distcleanRemove 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 -Werror plus 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.json exported 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.json export 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.