Build Systems
Build systems for systems programming including CMake, Meson, and Zig build with cross-compilation
You are an expert in build systems for systems-level code including CMake, Meson, and Zig build. ## Key Points - Use Ninja as the CMake generator for faster incremental builds (`cmake -G Ninja`). - Enable `CMAKE_EXPORT_COMPILE_COMMANDS` for IDE and clang-tidy integration. - Use CMake presets or Meson machine files to make builds reproducible across developers. - Prefer `FetchContent` (CMake) or wraps (Meson) over system-installed dependencies for reproducibility. - Pin dependency versions with hashes for supply chain security. - Use Zig as a C/C++ cross-compiler (`zig cc`) even in CMake/Meson projects when you need easy cross-compilation. - Separate build configuration from build logic; keep `CMakeLists.txt` or `meson.build` declarative. - Using `GLOB` for source files in CMake means added/removed files are not detected without re-running cmake. - Forgetting `target_` prefixed commands (e.g., using `include_directories` instead of `target_include_directories`) pollutes the global scope. - Meson does not support in-source builds; always use a separate build directory. - Zig's build cache can become stale after modifying `build.zig`; use `zig build --summary all` to debug. - Mixing Debug and Release libraries on Windows causes CRT mismatch link errors. ## Quick Example ```bash meson setup build-aarch64 --cross-file cross/aarch64-linux.ini meson compile -C build-aarch64 ```
skilldb get systems-programming-skills/Build SystemsFull skill: 350 linesBuild Systems — Systems Programming
You are an expert in build systems for systems-level code including CMake, Meson, and Zig build.
Core Philosophy
Overview
Build systems automate compilation, linking, dependency management, and cross-compilation for systems software. CMake is the dominant C/C++ build system with broad IDE support. Meson offers a cleaner syntax with fast Ninja-backed builds. Zig's build system is written in Zig itself, providing first-class cross-compilation and C/C++ integration. Choosing the right build system affects developer productivity, CI speed, and cross-platform support.
Core Concepts
CMake
CMake generates native build files (Makefiles, Ninja, Visual Studio projects) from a declarative CMakeLists.txt.
cmake_minimum_required(VERSION 3.25)
project(myapp VERSION 1.0.0 LANGUAGES C CXX)
set(CMAKE_C_STANDARD 17)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Library target
add_library(mylib STATIC
src/parser.cpp
src/lexer.cpp
src/codegen.cpp
)
target_include_directories(mylib PUBLIC include)
target_compile_options(mylib PRIVATE
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra -Wpedantic>
$<$<CXX_COMPILER_ID:MSVC>:/W4>
)
# Executable
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE mylib)
# External dependency via FetchContent
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.2.1
)
FetchContent_MakeAvailable(fmt)
target_link_libraries(myapp PRIVATE fmt::fmt)
# Testing
enable_testing()
add_executable(tests tests/test_parser.cpp tests/test_lexer.cpp)
target_link_libraries(tests PRIVATE mylib GTest::gtest_main)
include(GoogleTest)
gtest_discover_tests(tests)
# Install rules
install(TARGETS mylib myapp
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
)
install(DIRECTORY include/ DESTINATION include)
CMake presets for reproducible builds:
{
"version": 6,
"configurePresets": [
{
"name": "release",
"binaryDir": "${sourceDir}/build/release",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_INTERPROCEDURAL_OPTIMIZATION": "ON"
}
},
{
"name": "debug-asan",
"binaryDir": "${sourceDir}/build/debug-asan",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_C_FLAGS": "-fsanitize=address -fno-omit-frame-pointer",
"CMAKE_CXX_FLAGS": "-fsanitize=address -fno-omit-frame-pointer",
"CMAKE_EXE_LINKER_FLAGS": "-fsanitize=address"
}
}
],
"buildPresets": [
{
"name": "release",
"configurePreset": "release"
}
]
}
Meson
Meson provides a Python-like syntax with fast, correct builds.
# meson.build
project('myapp', 'c', 'cpp',
version: '1.0.0',
default_options: [
'c_std=c17',
'cpp_std=c++23',
'warning_level=3',
'buildtype=release',
]
)
# Dependencies
fmt_dep = dependency('fmt', version: '>=10.0', fallback: ['fmt', 'fmt_dep'])
threads_dep = dependency('threads')
# Library
mylib_sources = files(
'src/parser.cpp',
'src/lexer.cpp',
'src/codegen.cpp',
)
mylib = library('mylib', mylib_sources,
include_directories: include_directories('include'),
dependencies: [fmt_dep],
install: true,
)
mylib_dep = declare_dependency(
link_with: mylib,
include_directories: include_directories('include'),
)
# Executable
executable('myapp', 'src/main.cpp',
dependencies: [mylib_dep, threads_dep],
install: true,
)
# Tests
gtest_dep = dependency('gtest', main: true, required: false)
if gtest_dep.found()
test_exe = executable('tests',
'tests/test_parser.cpp',
'tests/test_lexer.cpp',
dependencies: [mylib_dep, gtest_dep],
)
test('unit tests', test_exe)
endif
# Subprojects for vendored deps (wraps)
# subprojects/fmt.wrap
Cross-compilation with Meson cross files:
# cross/aarch64-linux.ini
[binaries]
c = 'aarch64-linux-gnu-gcc'
cpp = 'aarch64-linux-gnu-g++'
ar = 'aarch64-linux-gnu-ar'
strip = 'aarch64-linux-gnu-strip'
[host_machine]
system = 'linux'
cpu_family = 'aarch64'
cpu = 'cortex-a72'
endian = 'little'
meson setup build-aarch64 --cross-file cross/aarch64-linux.ini
meson compile -C build-aarch64
Zig Build System
Zig's build system is a Zig program, enabling arbitrary build logic with cross-compilation built in.
// build.zig
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Static library
const lib = b.addStaticLibrary(.{
.name = "mylib",
.root_source_file = b.path("src/lib.zig"),
.target = target,
.optimize = optimize,
});
// Link C library
lib.linkSystemLibrary("sqlite3");
lib.addIncludePath(b.path("vendor/include"));
// Compile C sources alongside Zig
lib.addCSourceFiles(.{
.files = &.{
"vendor/stb_image.c",
"vendor/miniz.c",
},
.flags = &.{"-O2", "-DNDEBUG"},
});
b.installArtifact(lib);
// Executable
const exe = b.addExecutable(.{
.name = "myapp",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
exe.linkLibrary(lib);
b.installArtifact(exe);
// Run step
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| run_cmd.addArgs(args);
const run_step = b.step("run", "Run the application");
run_step.dependOn(&run_cmd.step);
// Tests
const tests = b.addTest(.{
.root_source_file = b.path("src/lib.zig"),
.target = target,
.optimize = optimize,
});
const run_tests = b.addRunArtifact(tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_tests.step);
}
Cross-compile with Zig (no extra toolchain needed):
# Build for ARM64 Linux from any host
zig build -Dtarget=aarch64-linux-gnu -Doptimize=ReleaseFast
# Build for Windows from Linux/macOS
zig build -Dtarget=x86_64-windows-msvc -Doptimize=ReleaseSafe
# Use Zig as a drop-in C cross-compiler
zig cc -target aarch64-linux-gnu -O2 -o hello hello.c
Implementation Patterns
CMake Superbuild with ExternalProject
include(ExternalProject)
ExternalProject_Add(zlib
URL https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz
URL_HASH SHA256=...
CMAKE_ARGS
-DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/deps
-DCMAKE_BUILD_TYPE=Release
)
ExternalProject_Add(myproject
SOURCE_DIR ${CMAKE_SOURCE_DIR}/src
CMAKE_ARGS
-DCMAKE_PREFIX_PATH=${CMAKE_BINARY_DIR}/deps
DEPENDS zlib
)
Conditional Compilation Across Build Systems
# CMake: platform-specific sources
if(WIN32)
target_sources(mylib PRIVATE src/platform_win32.cpp)
elseif(APPLE)
target_sources(mylib PRIVATE src/platform_macos.cpp)
else()
target_sources(mylib PRIVATE src/platform_linux.cpp)
endif()
// Zig: target-aware build logic
const target = b.standardTargetOptions(.{});
const os = target.result.os.tag;
if (os == .windows) {
exe.addCSourceFile(.{ .file = b.path("src/platform_win32.c"), .flags = &.{} });
exe.linkSystemLibrary("ws2_32");
} else if (os == .macos) {
exe.addFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" });
exe.linkFramework("CoreFoundation");
} else {
exe.addCSourceFile(.{ .file = b.path("src/platform_linux.c"), .flags = &.{} });
exe.linkSystemLibrary("pthread");
}
Best Practices
- Use Ninja as the CMake generator for faster incremental builds (
cmake -G Ninja). - Enable
CMAKE_EXPORT_COMPILE_COMMANDSfor IDE and clang-tidy integration. - Use CMake presets or Meson machine files to make builds reproducible across developers.
- Prefer
FetchContent(CMake) or wraps (Meson) over system-installed dependencies for reproducibility. - Pin dependency versions with hashes for supply chain security.
- Use Zig as a C/C++ cross-compiler (
zig cc) even in CMake/Meson projects when you need easy cross-compilation. - Separate build configuration from build logic; keep
CMakeLists.txtormeson.builddeclarative.
Common Pitfalls
- Using
GLOBfor source files in CMake means added/removed files are not detected without re-running cmake. - Forgetting
target_prefixed commands (e.g., usinginclude_directoriesinstead oftarget_include_directories) pollutes the global scope. - Meson does not support in-source builds; always use a separate build directory.
- Zig's build cache can become stale after modifying
build.zig; usezig build --summary allto debug. - Mixing Debug and Release libraries on Windows causes CRT mismatch link errors.
- Forgetting to set
CMAKE_POSITION_INDEPENDENT_CODE ONwhen building static libraries that will be linked into shared libraries.
Anti-Patterns
Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.
Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.
Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.
Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.
Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.
Install this skill directly: skilldb add systems-programming-skills
Related Skills
Cpp Modern
Modern C++ (C++20/23) patterns including concepts, ranges, coroutines, and modules
Ffi
Foreign function interfaces for calling between C, C++, Rust, Zig, Python, and other languages safely
Memory Safety
Techniques for achieving memory safety without garbage collection including ownership models, arenas, and static analysis
Zig Basics
Zig language fundamentals for systems programming including comptime, error handling, and manual memory management
Adversarial Code Review
Adversarial implementation review methodology that validates code completeness against requirements with fresh objectivity. Uses a coach-player dialectical loop to catch real gaps in security, logic, and data flow.
API Design Testing
Design, document, and test APIs following RESTful principles, consistent