I need to change build configuration

CNA snapshot 009d40f5  ·  Development › Maintainer Handbook  ·  source links pinned to 009d40f5

✓

Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Read at 009d40f5 from CMake files, presets, workflows and docs/build-performance.md; nothing was configured, built or run, and the named tests and CI cells are registrations, not results.

Use this recipe to add or change a CMake option, a default, a value of one of the three selectors, a renderer combination rule, a preset, a CI configuration or a pinned third-party source. CNA's configure step is also its validation step: a wrong combination is meant to fail at configure time with a message that names the option, and the decision code is written so that the refusal can be tested in milliseconds. The trace of the whole configure is on CMake architecture; this page is the procedure and the rules a change has to keep. Everything was read at 009d40f5 from CMake, presets and workflows; nothing was configured or run.

Find the owner

ChangeOwner fileWhat checks it
A root feature option (tests, diagnostics, video, net, CNAEXT, devices)CMakeLists.txtConfigure itself; presets; CI cells that set it
A value of CNA_PLATFORMPlatformSelection.cmake, PlatformX11.cmake, PlatformWayland.cmakeCnaWaylandPlatformSelection, CnaSdl2OnlyRendererGate, platform-ci.yml
A value of CNA_AUDIO_PLATFORMAudioPlatformSelection.cmakeCnaAudioPlatformSelection_* (script mode)
A renderer identity, default or setRendererIdentities.cmake, RendererDefaultSelection.cmake, RendererSelection.cmake, RendererRegistry.cmakeRendererIdentityRegistry, CnaRendererDefaultSelection_*, CnaRendererRetired_*, RuntimeRendererDiscipline, RendererTargetDiscipline
A pairwise renderer restrictionRendererCombinations.cmakeRendererCombinationRegistry (rule versus runtime-renderer-selection.md)
Whether SDL is configured at allSdlAvailability.cmake, ThirdPartySDL.cmakeCnaSdlOffFindsNoSdlPackage, the sdl-enable-matrix job
A pinned dependency, patch or prebuiltcmake/ThirdParty*.cmake, cmake/patches, SdlPrebuiltFingerprint.cmakeCnaSdlPrebuiltFingerprint
A presetCMakePresets.jsontools/build/check_build_performance_policy.py for the focused presets
Compiler, linker, sanitizer, debug-info, IPO, PCH, unity policyBuildPerformance.cmakeThe same policy checker; build-performance.md is the record

Read first

  1. CMakeLists.txt from the top through add_subdirectory(modules): the order is functional (selection precedes the SDL sub-build; the private compile policy is attached last to every CNA-owned target).
  2. The one selection file you are changing, and the cmake -P case that tests it (cmake/Tests).
  3. UnitTests.cmake around the CnaAudioPlatformSelection_* loop and ModuleProbes.cmake around CnaRendererDefaultSelection_*: the registration patterns.
  4. platform-ci.yml: the tuple matrix and the sdl-enable-matrix job that asserts a configure fails.
  5. Then the CMake option index, selection axes and the user-facing Building CNA: CMake options.

Rules an option must follow

ConcernRule at TARGETExample
NameProject options are CNA_<AREA>_<NAME>; a per-identity switch is CNA_RENDERER_<IDENTITY>; a per-renderer feature is CNA_<IDENTITY>_<FEATURE>. The chosen selector value becomes a compile definition CNA_PLATFORM_<NAME>, CNA_AUDIO_PLATFORM_<NAME> or CNA_RENDERER_<NAME>CNA_OPENGL4_COMPILED_EFFECTS, CNA_SDL_GPU_SHADERCROSS
TypeA boolean uses option(). Anything with more than two states is a CACHE STRING with set_property(CACHE ... STRINGS ...), upper-cased, validated against a regex and written back with FORCE so the cache holds the normal formCNA_DIAGNOSTICS (OFF, STATS, FULL), CNA_ENABLE_VIDEO, CNA_ENABLE_SDL (AUTO, ON, OFF)
DefaultThe default reproduces the historical build byte for byte. AUTO means “as before”; a new capability is OFF or AUTO until it is proven, and defaults change only with the same evidenceThe comment header of SdlAvailability.cmake
RefusalRefuse an invalid value or combination with message(FATAL_ERROR) that names the option, the value received, the valid set and why nothing is substituted. Never fall back to another identityReserved identifiers (SDL12, EMSCRIPTEN, OPENAL, WASAPI) are recognised so that they fail loudly instead of reading as typos
TestabilityPut the decision in its own file that works under cmake -P (cmake_policy(SET CMP0057 NEW), include_guard, guard directory-scoped commands with CMAKE_SCRIPT_MODE_FILE), then test it without configuring a projectAudioPlatformSelection.cmake, RendererDefaultSelection.cmake, RendererIdentities.cmake
Message wording in testsAssert a short, stable phrase: CMake wraps long diagnostics at terminal width, so a long expected string is never found"is a reserved identifier", "is not a member of"
Dependent optionsState a required pairing at configure time, with the option to change, rather than as a later missing includeCNA_BUILD_C_API=ON requires CNA_ENABLE_NET=ON
DocumentationNo gate reads every option. Two are held mechanically: renderer combination rules against docs/runtime-renderer-selection.md, and the documented renderer count against the identity table. Update the option's docs/*.md, the presets and the site's option table yourselfcheck_renderer_combinations.py

The shape of a tri-state option, as CNA_DIAGNOSTICS and CNA_ENABLE_VIDEO write it (a template for a new option, not an existing one):

set(CNA_ENABLE_FOO "AUTO" CACHE STRING "Enable Foo (OFF, AUTO, or ON)")
set_property(CACHE CNA_ENABLE_FOO PROPERTY STRINGS OFF AUTO ON)
string(TOUPPER "${CNA_ENABLE_FOO}" _cna_enable_foo_normalized)
if(NOT _cna_enable_foo_normalized MATCHES "^(OFF|AUTO|ON)$")
    message(FATAL_ERROR
        "CNA_ENABLE_FOO must be OFF, AUTO, or ON (received '${CNA_ENABLE_FOO}').")
endif()
set(CNA_ENABLE_FOO "${_cna_enable_foo_normalized}" CACHE STRING
    "Enable Foo (OFF, AUTO, or ON)" FORCE)
unset(_cna_enable_foo_normalized)

The three selectors and their refusals

SelectorValues at TARGETRefused at configure timeRefusal test
CNA_PLATFORM (default SDL3)SDL3, SDL2, HEADLESS always; WIN32 on Windows targets, TERMINAL otherwise; X11 and WAYLAND only where their development packages are detectedReserved SDL12 and EMSCRIPTEN (and the host-conditional pair on the wrong host); X11 or Wayland without packages, naming what to install and never falling back to SDL3 or Xwayland; unknown values; TERMINAL with a GPU renderer (checked in RendererSelection.cmake)CnaWaylandPlatformSelection (nested configure), CnaSdl2OnlyRendererGate
CNA_AUDIO_PLATFORM (default SDL3)SDL3, SDL2, NULL, ALSAReserved OPENAL and WASAPI; unknown values; ALSA off LinuxEight cmake -P cases: CnaAudioPlatformSelection_DEFAULT, _SDL3, _SDL2, _NULL, _ALSA, _OPENAL, _WASAPI, _BOGUS
CNA_GRAPHICS_RENDERER and optional CNA_GRAPHICS_RENDERERSThe 25 public identities; the default is Emscripten WEBGL2, Linux OPENGLES3, otherwise SDL_RENDERERAny name outside the 25 given to the cache selector or as a set member, and any retired identity on all three routes (cache selector, set member, CNA_RENDERER_<X>=ON; a removed identity gets its own message that says it was removed); a CNA_RENDERER_<X>=ON switch for a name that is neither public nor retired is not read and is not refused; a default that is not a member of the set; unbuildable pairs; per-identity host and dependency gatesCnaRendererDefaultSelection_*, CnaRendererRetired_* (generated from the retired list), RendererIdentityRegistry, RendererCombinationRegistry
CNA_ENABLE_SDL (AUTO, ON, OFF)A switch layered over all threeOFF while any selection genuinely needs SDL (an SDL platform or audio value, or SDL_RENDERER, SDL_GPU, FNA3D, FREEDIRECT), listing the reasonsCnaSdlOffFindsNoSdlPackage, four cells of sdl-enable-matrix

The selectors are independent by design. The one hidden coupling is deliberate and refused explicitly in Sdl2OnlyConfiguration.cmake: an SDL2 platform with SDL3 audio (and the reverse) would put two SDL generations exporting the same symbols in one process, and SDL2 platform plus SDL2 audio also refuses the renderers that link SDL3 directly. A new coupling between selectors is a design change and needs an entry in the selection axes index.

Renderer combination rules and the registry

  • Combination rules are real, demonstrated conflicts with a stated reason. At TARGET: PORTABLEGL cannot join a real-GL renderer (it defines the global gl* symbols); GDI cannot join SOFTWARE (it recompiles the same translation units with a different definition); and identities cannot span the Windows-only, Emscripten-only and macOS-only partitions. Five EasyGL identities can coexist since GL profile became a runtime value, although a variable and a comment in RendererCombinations.cmake still describe the removed rule. The rejection function joins its reason from ARGN: write the reason as adjacent literals with no ;, which CMake would split.
  • The registry is generated. cna_generate_renderer_registry writes generated/CnaRendererRegistry.generated.cpp into the build tree from the identity map in RendererRegistry.cmake; an identity with no row is a configure error naming the three places to register it. The explicit table exists because a static-initialiser self-registration would be discarded by the linker inside an archive. The first identity is the build's default.
  • Identity macros. An arm of cna_configure_renderer_identity appends its macro to _cna_identity_defines; it must never call add_compile_definitions, which is directory-scoped and would define that identity's macro for every renderer in the set. scripts/check_runtime_renderer_discipline.py (RuntimeRendererDiscipline) fails on a new one.
  • Target gates versus resource gates. A gate that decides whether a target exists and runs against the default renderer must test equality with CNA_GRAPHICS_RENDERER; a gate that decides whether a resource is available may test membership in the set (graphics examples). In a single-renderer build they coincide, which is why the mistake is easy to make.
  • Adding a whole renderer family is the worked example on CMake architecture; do not copy a neighbour's family CMake without knowing which special relationship applies.

Prove a selector change

  1. A script-mode case for the decision. Follow the audio loop in UnitTests.cmake: add_test(NAME CnaAudioPlatformSelection_<CASE> COMMAND cmake -DCNA_AUDIO_SELECTION_FILE=... -DCNA_AUDIO_SELECTION_CASE=<CASE> -DCNA_AUDIO_SELECTION_EXPECTED=<phrase> -P cmake/Tests/AudioPlatformSelectionCase.cmake) with the audio;platform labels. Cover the default, every implemented value, every reserved value and an unknown value. The renderer default cases carry an outcome (ACCEPT or REJECT) and, for refusal, expected text. Keep the control cases (an accepted case, an unknown name still unknown): a refusal that refuses everything would pass every reject case.
  2. A nested configure only where detection or a real project is needed (CnaWaylandPlatformSelection, CnaSdlOffFindsNoSdlPackage configure a work directory with the generator under test).
  3. A CI cell for the expected outcome. Copy the sdl-enable-matrix pattern: a matrix row with expect: configure-succeeds|configure-fails, configure with set +e, then assert the status and, for a failure, grep the diagnostic. It proves the refusal, and builds nothing.
  4. Single, multi and default. Configure a single-identity tree, a multi-renderer set with a different default, and the unchanged default; inspect the generated registry and the final link line. scripts/check_renderer_configure_sweep.sh configures every identity (skipping ones whose SDK is absent) without building.
  5. Run the identity, combination and discipline checks (Python) after any list edit; check_renderer_identities.py follows each identity through to its descriptor accessor and checks documented counts.
⚠

Retired names. A name outside the 25 is refused by name; the refusal cases are generated from the retired list so retiring an identity adds its test automatically, and a retired C ABI value is never reused. Do not write a removed identity into documentation or tests as a live example.

Presets, CI and documentation

  • Presets. CMakePresets.json has 17 visible configure presets and a hidden base-ninja parent (Ninja, CNA_EXPORT_COMPILE_COMMANDS, CNA_USE_CCACHE), and its own list of 17 build presets; macos, ios, ios-simulator and multi-renderer have no build preset. Binary directories are ${sourceDir}/cmake-build-<name>. The dev, unit and release-modules closures deliberately omit tests, demos, the C API, networking, FFmpeg and Draco; PCH, unity and IPO stay opt-in presets. tools/build/check_build_performance_policy.py (run in general-tests-ci.yml) resolves preset inheritance and fails if those closures regain the omitted features or if routine policy is written into global CMAKE_CXX_FLAGS, linker flags or add_compile_options.
  • Workflows. The selection matrix lives in platform-ci.yml (SDL3 + OPENGLES3, SDL2 + OPENGLES3 with SDL2 audio, SDL3 + VULKAN, SDL3 + SOFTWARE, HEADLESS with NULL audio, TERMINAL + SOFTWARE with NULL audio, SDL-free X11 cells with NULL and ALSA audio, Win32 under Wine and native, and the SDL-switch job). multi-renderer-ci.yml builds HEADLESS;SOFTWARE;STUB and a single-renderer control, because the property that matters is that single-renderer builds did not change. Path filters exclude Markdown and docs/. Sibling checkouts use clone_siblings.sh (candidate branches, then next, then develop). Twenty workflow files exist; the C API has five build-free gate workflows and no build workflow (Update the C API).
  • Documentation. A user-visible option needs the site's options table, the presets and, for renderers, the renderer guide; the generated option index is regenerated from the source, not edited.

Third-party sources, patches and siblings

DependencyWhere and how pinnedOffline or override
SDL3, SDL3_image, SDL3_mixerSubmodules, built at configure time into a persistent prebuilt root outside every build treeCNA_USE_SYSTEM_SDL=ON; CNA_SDL_PREBUILT_ROOT; CNA_MAX_VENDORED_BUILD_JOBS (default 2, separate from Ninja parallelism)
FNA3D and MojoShaderCNA_FNA3D_GIT_TAG; a series of local patches against one MojoShader revision applied as a whole, idempotently, with a stamp-DFETCHCONTENT_SOURCE_DIR_FNA3D=<path>
wgpu-native, PortableGL, SDL2, SDL_shadercross and SPIRV-CrossPinned versions or commits in their own ThirdParty*.cmake; wgpu-native downloads verify a SHA-256 and fail closedCNA_WEBGPU_ROOT with auto-download off; FETCHCONTENT_SOURCE_DIR_*; CNA_SDL2_ROOT
sharp-runtime, easy-gl, meta-gl, free-directSibling checkouts, not submodules; sharp-runtime must be its next branch; CNA requests a fixed component closureCNA_SHARP_RUNTIME_ROOT

Recently added mechanisms at this snapshot: the persistent SDL prebuilt is now reused only while a build manifest still matches (SdlPrebuiltFingerprint.cmake: SHA-256 over the vendored source tree without .git, every patch in order with its hash, and every sub-build argument; ccache launcher excluded), so a moved source, an edited patch or changed arguments rebuild it once and an interrupted build is retried; two SDL_gpu Vulkan barrier patches are staged from a copy so the vendored tree stays pristine (cmake/patches); CNA_OPENGL4_COMPILED_EFFECTS was added in the shape of the other eight compiled-effect options; with CNA_ENABLE_SDL=OFF MojoShader now builds on the C library without its SDL_gpu adapter (FNA3D still needs SDL and is refused by name); the EasyGL test filter lists only the five EasyGL identities; and an OpenGL4 GL-error output gate joins the Vulkan validation gate. Both output gates turn a printed message into a test failure through FAIL_REGULAR_EXPRESSION, for problems that appear at teardown beyond any in-process assertion.

To change a pin: change the tag or commit, refresh any patch series against exactly that revision (a patch that does not apply stops the configure and names itself), and rely on the manifest to rebuild the SDL prebuilt; to force it, delete the prebuilt root that configure prints. The C API package is the only installable target (modules/c-api/CMakeLists.txt): the C++ framework has no install() rules.

Shared library and build performance

  • CNA_SHARED_LIBRARY defaults ON on native ELF with GNU or Clang and CMake 3.27 or newer; the decision precedes every target because archives inside libcna.so need position-independent code, and it is a configure error elsewhere. The C API links module archives directly and stays self-contained. CNA_ENABLE_IPO is refused with sanitizers and with CNA_C_API_BUILD_STATIC=ON.
  • ccache. CNA_USE_CCACHE (ON) wraps compilers with a launcher that exports CCACHE_BASEDIR from CNA_CCACHE_BASEDIR (the environment's value wins when set; otherwise the parent of the source directory), so the same translation unit hashes identically across build trees; cmake --build <dir> --target cna_ccache_stats shows the cache. CNA does not change global ccache sloppiness, and it documents an apparent 0% direct-hit rate that turned out to be an unwritable statistics directory. Read counters as deltas, not lifetime rates.
  • Focused targets and linkers. Module test executables share object libraries with CnaTests; CNA_LINKER selects AUTO (Mold, then LLD), DEFAULT, MOLD or LLD; CNA_DEBUG_INFO=LINE_TABLES and the PCH and unity pilots are opt-in and measured in docs/build-performance.md. Compare a change only against the same compiler, renderer, cache state and target.

Traps

  • An unread cache entry. Forgetting a removed identity would make -DCNA_RENDERER_X=ON silently configure the default; that is why each retired name is checked on that route too, and why a value not consumed anywhere is worse than a refusal. The check does not reach a misspelled or never-existing name: -DCNA_RENDERER_D3D9=ON is still an unread cache entry at this snapshot, so read the CNA: Using <X> graphics renderer line after such a configure.
  • A default that moves. The per-host default lives in one file used by two consumers so the SDL-free decision cannot drift from the renderer default; do not restate it.
  • Stale comments in selection files. PlatformSelection.cmake says “two of the five” while seven platforms exist; ModuleProbes.cmake still speaks of 42 identities; RendererCombinations.cmake describes a rule P11 removed. Trust the code and the identity table.
  • Presets that hide a failure. A preset is a convenience, not a matrix: dev is not evidence for a renderer, C API, network, media or Draco change (What to test after changing X).
  • Directory-scoped commands in selection code. add_compile_definitions and friends leak across the set and do not run in script mode.
  • A nested sub-build inherits nothing. SDL is configured by separate CMake invocations; settle Apple sysroot and architecture before launching them.

Review checklist

  • The option's name, type, default and normal form follow the table; the default build is unchanged.
  • Every invalid value or combination fails at configure time by name; nothing is substituted.
  • A script-mode or nested test asserts the refusal with a short phrase, plus a control that accepts; a CI cell asserts the expected outcome where a real configure is needed.
  • Presets, workflow cells, the site option table and, for renderers, the combination document agree; single, multi and default configurations were considered.
  • Pins, patches and offline overrides are recorded; anything not configured or run is stated as such.

Related: Blast radius and readiness, Test architecture, Fix a renderer bug and Make a release.

The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.

Architecture
CMake architecture
Tests and validation
Test architecture