Skip to main content
Technology & EngineeringSystems Programming350 lines

Build Systems

Build systems for systems programming including CMake, Meson, and Zig build with cross-compilation

Quick Summary25 lines
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 lines
Paste into your CLAUDE.md or agent config

Build 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_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.

Common Pitfalls

  • 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.
  • Forgetting to set CMAKE_POSITION_INDEPENDENT_CODE ON when 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

Get CLI access →