Skip to main content
Technology & EngineeringSystems Programming349 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: 349 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 →

Related Skills

Cpp Modern

Modern C++ (C++20/23) patterns including concepts, ranges, coroutines, and modules

Systems Programming293L

Ffi

Foreign function interfaces for calling between C, C++, Rust, Zig, Python, and other languages safely

Systems Programming301L

Memory Safety

Techniques for achieving memory safety without garbage collection including ownership models, arenas, and static analysis

Systems Programming311L

Zig Basics

Zig language fundamentals for systems programming including comptime, error handling, and manual memory management

Systems Programming236L

Building a Polite Crawler

Activate this skill when the user is writing a crawler, scraper or fetcher of any size and wants it to be identifiable, cheap for the sites it visits, resilient to rate limiting, and defensible. Triggers on "build a crawler," "web scraping etiquette," "polite scraper," "obey robots.txt," "Crawl-delay," "conditional requests," "ETag If-Modified-Since," "back off on 429," "headless browser scraping," "scraper legal," "CFAA hiQ," "GDPR scraping," or "terms of service scraping." Covers self-identification, robots.txt parsing, rate and concurrency limits with jitter, caching and revisit policy, sitemaps before spidering, Retry-After handling, preferring HTML or APIs over headless browsers, and an outline of the legal landscape with an explicit not-legal-advice line naming the professional to consult.

Bot Traffic and Crawler Defense177L

Content Exposure and Preview Gating

Triggers when the user wants to know exactly what their site hands to any client that asks, or wants to gate full text behind a preview without destroying search visibility. Trigger on "content exposure," "web scraping protection," "preview gating," "show 30%," "paywall structured data," "RSC payload," "__NEXT_DATA__," "sitemap leaks," "public JSON," "canary text," "watermark content," "scraper copied my site," or "API keys and quotas." Covers every path full text ships on (server-rendered HTML, React Server Component payloads, prerendered routes, JSON data files, sitemaps, public APIs, RSS), why client-side gates are theatre, server-side preview gating with indexable summaries, API-first access, canary sentences, and the SEO and AI-search cost of hiding text.

Bot Traffic and Crawler Defense165L