CMake Usage Requirements: Propagate ABI-Critical Definitions to Downstream Packages
If a public header changes layout under a compile definition, the library and its consumers must receive a compatible value. Setting the definition only while building the library can produce a successful link with a mismatched consumer. Express the requirement on the CMake target, then verify the actual downstream compile command.
This guide uses the CMake 3.31.12 documentation served in the versioned 3.31 manual. CMake itself was unavailable here. An executed manual GCC fixture demonstrates the ABI consequence of a missing definition; the supplied CMake propagation checks remain proposed.
Classify each setting by who needs it
Review public headers before classifying flags. A definition controlling structure fields, enum representation or an inline interface can be a consumer requirement. A definition enabling private logging inside one source file usually is not.
The target_compile_definitions reference separates PRIVATE, PUBLIC and INTERFACE scope. PRIVATE applies to the target’s own compilation. INTERFACE describes consumers. PUBLIC does both. These scopes are about propagation, not the sensitivity or accessibility of the macro’s name.
| Definition scope | Producer compilation | Consumer compilation |
|---|---|---|
| PRIVATE | Receives definition | Does not inherit this definition |
| INTERFACE | Does not receive it from this declaration | Inherits definition through the target |
| PUBLIC | Receives definition | Inherits definition through the target |
The buildsystem manual stresses that usage requirements must be requirements, rather than convenient preferences imposed on downstream projects. Do not export every warning, optimization option or internal feature toggle merely to make two command lines look identical.

Model a public layout definition
The fixture header defines api_record with a fixed tag and a value field selected by API_WIDE. With API_WIDE=1, the value uses uint64_t; otherwise it uses uint32_t. The provider exposes a function reporting its compiled sizeof(api_record).
The consumer reports its own size and compares the two integers. It does not pass a mismatched structure across the boundary. That keeps the negative test diagnostic rather than executing an incompatible data exchange.
Manual GCC 14.2.0 compilation on x86-64 produced a 16-byte provider record with API_WIDE=1. A consumer compiled with zero saw eight bytes and returned a mismatch result. A consumer compiled with one saw sixteen bytes and passed. These sizes are host observations, not a layout formula for every target.
Express the requirement through the target
For this hypothetical interface, the intended CMake declaration is target_compile_definitions(api PUBLIC API_WIDE=1). The library target also publishes the required header directory. The consumer then links the target through target_link_libraries(consumer PRIVATE api).
The PRIVATE keyword on that consumer link does not prevent consumer from receiving api’s usage requirements. It controls whether that dependency relationship is exposed onward. The target_link_libraries documentation and buildsystem model describe the target-based transitive relationship.
Keep the generated build commands as the evidence. A correct-looking CMakeLists.txt can still be bypassed by a consumer that links a raw archive pathname or invokes the compiler outside the exported target. An installed library file alone does not carry every header compilation requirement.
Use three proposed scope controls
Configure separate fresh build directories for PRIVATE, INTERFACE and PUBLIC variants of the definition. Hold the source and target relationship fixed. Inspect the provider and consumer compile commands, then run the safe size-report comparison where host execution is appropriate.
PRIVATE should leave the consumer without the required inherited definition in this example. INTERFACE should leave the producer without it unless another input supplies it. PUBLIC should supply both. Check the effective values instead of assuming that an omitted option means zero when a toolchain file or directory-level command could define it elsewhere.
These CMake cases were not executed here. The manual C fixture establishes why the definition matters; it does not establish that a particular generator or installed package propagated it correctly.
Audit more than compile definitions
Public headers can require include directories, language features and dependent target interfaces as well. Record those requirements in a small producer/consumer contract table. Use supported target commands rather than concatenating opaque command-line strings into one global flag variable.
Keep link closure separate from compile-interface visibility. A static library’s implementation dependency may still be needed at the final executable link even when its compile requirements should remain private. Do not simplify PRIVATE into “the downstream link never needs this library.”
Where multiple dependencies impose incompatible ABI settings, make that conflict visible. CMake’s compatible-interface facilities can represent selected interface properties, but they do not automatically understand the meaning of every arbitrary macro. Preserve a semantic compatibility test alongside buildsystem checks.
Test the installed consumer path
A same-tree build can accidentally inherit directory-level definitions or build-tree include paths. Export and install the package’s intended targets into an owned test prefix, then configure a separate consumer using only the installed package interface.
Check generated package files for absolute producer build paths. Ensure build-interface and install-interface requirements refer to the correct locations. Repeat in each supported configuration when generator expressions select different definitions or dependencies.
For a cross-build, inspect the target compile commands and artifacts without running them on the host. Execute the boundary test only in the approved target or emulator environment, and retain that environment’s identity. A host size comparison does not qualify a different word size or ABI.
Make the compilation contract a release artifact
Preserve the public header, interface target properties, actual producer/consumer commands and the installed-consumer test. Review changes to ABI-critical definitions as interface changes even when the exported symbol names remain unchanged.
The acceptance criterion is that every supported consumer receives the requirements needed to interpret the public interface, with conflicts detected before data crosses the boundary. Here the safe manual mismatch was demonstrated, while the CMake and target propagation steps remain explicit work to execute.