Skip to content

Ceedling’s Delta Builds

Like many source build systems, Ceedling seeks to avoid unnecessary actions when nothing in a project has changed.

Ceedling’s delta builds are automatic. No configuration is needed.

Tip

Fully clean builds can be triggered by first running ceedling clobber.

Delta build overview

Ceedling builds and maintains a cache and a dependency tree to avoid unnecessary file generation, toolchain actions (preprocessing, compiling, linking), and running of test fixtures.

The checks in the dependency tree are content based, not timestamp based, and include:

  1. File content changes. Did an antecedent of a build target change? That is, did a file that other build targets are dependent on change and, thus, require that the target be rebuilt?
  2. Configuration changes. Did a build target’s project configuration change? That is, even if all files remain unchanged, did a project configuration change impact one or more build targets?

Ceedling builds the dependency graph for you from two sources:

  1. Dependency files generated by compilation. The toolchain’s search paths and “spidering” among files during compilation produces a dependency graph as a side effect. Ceedling captures this and uses it to understand the relationships among your C files to trigger rebuilds.
  2. Its own understanding of your project through the lens of its build pipeline. In test builds, Ceedling and its supporting frameworks generate various files from various sources. Ceedling’s built-in understanding of those file generation relationships also drive individual file target rebuilds.

Where a C file change ripples out only along that file’s dependency chain, a shared configuration change ripples out across every target that reads that piece of configuration — often the whole project. File changes tend to trigger depth-oriented rebuilds while configuration changes tend to trigger breadth-oriented rebuilds.

Delta build special cases

Add the --force-test-rerun flag to test build tasks to bypass the checks for only running changed test fixture executables. This command line flag forces those executables to run and produce fresh test results.

When Unity’s test shuffling feature is enabled, delta builds automatically rerun test fixture executables to randomize test case order regardless of change or lack thereof in a test’s dependency tree. That is, since the feature requires test executables to run in order to shuffle test case order, effectively, delta builds are disabled for test fixture executables.

Delta build examples

Say a project has a sensor module (sensor.c / sensor.h) and a logger module (logger.c / logger.h), each with its own test file. test_sensor.c mocks sensor.h while test_logger.c has nothing to do with sensor.h at all.

Header file change

You add a new function prototype to sensor.h. On the next build, Ceedling walks its dependency graph and finds every target that lists sensor.h as an antecedent:

  • mock_sensor.c / mock_sensor.h — regenerated, since the mock is derived from sensor.h’s contents.
  • sensor.c — recompiled, since it includes sensor.h directly.
  • test_sensor.c — recompiled, since it also includes sensor.h (via the mock).
  • test_sensor executable — relinked and rerun, since its objects changed.

logger.c and test_logger.c never reference sensor.h. Nothing about them changed. Ceedling leaves that whole chain untouched and doesn’t rebuild or rerun test_logger but will report its cached test results.

flowchart LR
    subgraph changed["Changed file"]
        H["sensor.h"]
    end

    H --> M["mock_sensor.c / .h<br/>(regenerated)"]
    H --> SC["sensor.c<br/>(recompiled)"]
    M --> TC["test_sensor.c<br/>(recompiled)"]
    SC --> TL["test_sensor<br/>(relinked, rerun)"]
    TC --> TL

    subgraph untouched["Untouched"]
        LH["logger.h"] --> LC["logger.c"]
        LC --> TLog["test_logger<br/>(unchanged, not rerun)"]
    end

Configuration change

Now say you add a a new warning flag under :flags ↳ :test ↳ :compile in your Ceedling project configuration. No source file changed at all.

A compile flag is part of a compiled object’s configuration and is tracked as metadata rather than as a file antecedent. Changing this flag invalidates every object built with that flag set, project-wide, regardless of whether the object’s own source changed.

  • Every test source’s object file — test_sensor.o, test_logger.o, and any others — is recompiled with the new flag.
  • Every module source’s object file compiled into a test build is likewise recompiled.
  • Both test executables are relinked and rerun, since their objects changed.
flowchart LR
    subgraph changed["Changed configuration"]
        F[":flags ↳ :test ↳ :compile"]
    end

    F --> SO["sensor.o<br/>(recompiled)"]
    F --> LO["logger.o<br/>(recompiled)"]
    F --> TSO["test_sensor.o<br/>(recompiled)"]
    F --> TLO["test_logger.o<br/>(recompiled)"]

    SO --> TS["test_sensor<br/>(relinked, rerun)"]
    TSO --> TS
    LO --> TL2["test_logger<br/>(relinked, rerun)"]
    TLO --> TL2