I need to change build configuration
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
| Change | Owner file | What checks it |
|---|---|---|
| A root feature option (tests, diagnostics, video, net, CNAEXT, devices) | CMakeLists.txt | Configure itself; presets; CI cells that set it |
A value of CNA_PLATFORM | PlatformSelection.cmake, PlatformX11.cmake, PlatformWayland.cmake | CnaWaylandPlatformSelection, CnaSdl2OnlyRendererGate, platform-ci.yml |
A value of CNA_AUDIO_PLATFORM | AudioPlatformSelection.cmake | CnaAudioPlatformSelection_* (script mode) |
| A renderer identity, default or set | RendererIdentities.cmake, RendererDefaultSelection.cmake, RendererSelection.cmake, RendererRegistry.cmake | RendererIdentityRegistry, CnaRendererDefaultSelection_*, CnaRendererRetired_*, RuntimeRendererDiscipline, RendererTargetDiscipline |
| A pairwise renderer restriction | RendererCombinations.cmake | RendererCombinationRegistry (rule versus runtime-renderer-selection.md) |
| Whether SDL is configured at all | SdlAvailability.cmake, ThirdPartySDL.cmake | CnaSdlOffFindsNoSdlPackage, the sdl-enable-matrix job |
| A pinned dependency, patch or prebuilt | cmake/ThirdParty*.cmake, cmake/patches, SdlPrebuiltFingerprint.cmake | CnaSdlPrebuiltFingerprint |
| A preset | CMakePresets.json | tools/build/check_build_performance_policy.py for the focused presets |
| Compiler, linker, sanitizer, debug-info, IPO, PCH, unity policy | BuildPerformance.cmake | The same policy checker; build-performance.md is the record |
Read first
CMakeLists.txtfrom the top throughadd_subdirectory(modules): the order is functional (selection precedes the SDL sub-build; the private compile policy is attached last to every CNA-owned target).- The one selection file you are changing, and the
cmake -Pcase that tests it (cmake/Tests). UnitTests.cmakearound theCnaAudioPlatformSelection_*loop andModuleProbes.cmakearoundCnaRendererDefaultSelection_*: the registration patterns.platform-ci.yml: the tuple matrix and thesdl-enable-matrixjob that asserts a configure fails.- Then the CMake option index, selection axes and the user-facing Building CNA: CMake options.
Rules an option must follow
| Concern | Rule at TARGET | Example |
|---|---|---|
| Name | Project 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 |
| Type | A 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 form | CNA_DIAGNOSTICS (OFF, STATS, FULL), CNA_ENABLE_VIDEO, CNA_ENABLE_SDL (AUTO, ON, OFF) |
| Default | The 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 evidence | The comment header of SdlAvailability.cmake |
| Refusal | Refuse 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 identity | Reserved identifiers (SDL12, EMSCRIPTEN, OPENAL, WASAPI) are recognised so that they fail loudly instead of reading as typos |
| Testability | Put 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 project | AudioPlatformSelection.cmake, RendererDefaultSelection.cmake, RendererIdentities.cmake |
| Message wording in tests | Assert 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 options | State a required pairing at configure time, with the option to change, rather than as a later missing include | CNA_BUILD_C_API=ON requires CNA_ENABLE_NET=ON |
| Documentation | No 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 yourself | check_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
| Selector | Values at TARGET | Refused at configure time | Refusal test |
|---|---|---|---|
CNA_PLATFORM (default SDL3) | SDL3, SDL2, HEADLESS always; WIN32 on Windows targets, TERMINAL otherwise; X11 and WAYLAND only where their development packages are detected | Reserved 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, ALSA | Reserved OPENAL and WASAPI; unknown values; ALSA off Linux | Eight cmake -P cases: CnaAudioPlatformSelection_DEFAULT, _SDL3, _SDL2, _NULL, _ALSA, _OPENAL, _WASAPI, _BOGUS |
CNA_GRAPHICS_RENDERER and optional CNA_GRAPHICS_RENDERERS | The 25 public identities; the default is Emscripten WEBGL2, Linux OPENGLES3, otherwise SDL_RENDERER | Any 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 gates | CnaRendererDefaultSelection_*, CnaRendererRetired_* (generated from the retired list), RendererIdentityRegistry, RendererCombinationRegistry |
CNA_ENABLE_SDL (AUTO, ON, OFF) | A switch layered over all three | OFF while any selection genuinely needs SDL (an SDL platform or audio value, or SDL_RENDERER, SDL_GPU, FNA3D, FREEDIRECT), listing the reasons | CnaSdlOffFindsNoSdlPackage, 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:
PORTABLEGLcannot join a real-GL renderer (it defines the globalgl*symbols);GDIcannot joinSOFTWARE(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 inRendererCombinations.cmakestill describe the removed rule. The rejection function joins its reason fromARGN: write the reason as adjacent literals with no;, which CMake would split. - The registry is generated.
cna_generate_renderer_registrywritesgenerated/CnaRendererRegistry.generated.cppinto the build tree from the identity map inRendererRegistry.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_identityappends its macro to_cna_identity_defines; it must never calladd_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
- 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 theaudio;platformlabels. Cover the default, every implemented value, every reserved value and an unknown value. The renderer default cases carry an outcome (ACCEPTorREJECT) 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. - A nested configure only where detection or a real project is needed (
CnaWaylandPlatformSelection,CnaSdlOffFindsNoSdlPackageconfigure a work directory with the generator under test). - A CI cell for the expected outcome. Copy the
sdl-enable-matrixpattern: a matrix row withexpect: configure-succeeds|configure-fails, configure withset +e, then assert the status and, for a failure,grepthe diagnostic. It proves the refusal, and builds nothing. - 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.shconfigures every identity (skipping ones whose SDK is absent) without building. - Run the identity, combination and discipline checks (Python) after any list edit;
check_renderer_identities.pyfollows 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.jsonhas 17 visible configure presets and a hiddenbase-ninjaparent (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>. Thedev,unitandrelease-modulesclosures 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 ingeneral-tests-ci.yml) resolves preset inheritance and fails if those closures regain the omitted features or if routine policy is written into globalCMAKE_CXX_FLAGS, linker flags oradd_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.ymlbuildsHEADLESS;SOFTWARE;STUBand a single-renderer control, because the property that matters is that single-renderer builds did not change. Path filters exclude Markdown anddocs/. Sibling checkouts useclone_siblings.sh(candidate branches, thennext, thendevelop). 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
| Dependency | Where and how pinned | Offline or override |
|---|---|---|
| SDL3, SDL3_image, SDL3_mixer | Submodules, built at configure time into a persistent prebuilt root outside every build tree | CNA_USE_SYSTEM_SDL=ON; CNA_SDL_PREBUILT_ROOT; CNA_MAX_VENDORED_BUILD_JOBS (default 2, separate from Ninja parallelism) |
| FNA3D and MojoShader | CNA_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-Cross | Pinned versions or commits in their own ThirdParty*.cmake; wgpu-native downloads verify a SHA-256 and fail closed | CNA_WEBGPU_ROOT with auto-download off; FETCHCONTENT_SOURCE_DIR_*; CNA_SDL2_ROOT |
| sharp-runtime, easy-gl, meta-gl, free-direct | Sibling checkouts, not submodules; sharp-runtime must be its next branch; CNA requests a fixed component closure | CNA_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_LIBRARYdefaults ON on native ELF with GNU or Clang and CMake 3.27 or newer; the decision precedes every target because archives insidelibcna.soneed position-independent code, and it is a configure error elsewhere. The C API links module archives directly and stays self-contained.CNA_ENABLE_IPOis refused with sanitizers and withCNA_C_API_BUILD_STATIC=ON.- ccache.
CNA_USE_CCACHE(ON) wraps compilers with a launcher that exportsCCACHE_BASEDIRfromCNA_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_statsshows 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_LINKERselects AUTO (Mold, then LLD), DEFAULT, MOLD or LLD;CNA_DEBUG_INFO=LINE_TABLESand the PCH and unity pilots are opt-in and measured indocs/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=ONsilently 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=ONis still an unread cache entry at this snapshot, so read theCNA: Using <X> graphics rendererline 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.cmakesays “two of the five” while seven platforms exist;ModuleProbes.cmakestill speaks of 42 identities;RendererCombinations.cmakedescribes a rule P11 removed. Trust the code and the identity table. - Presets that hide a failure. A preset is a convenience, not a matrix:
devis 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_definitionsand 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.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Configuring CNA per target: routes, toolchains and what a green build proves — How the Linux, Windows (MSVC and MinGW-w64 with Wine), Android, Emscripten and Apple routes configure CNA, run its tests, and what a successful configure, build or test run on each one proves.
- Dependency acquisition: submodules, siblings, fetched pins and host packages — The five ways outside code enters a CNA build, how they compose, what the configure-time SDL build implies, how host packages change what compiles, the sibling contract and how CNA itself is consumed.
- easy-gl and meta-gl: the two-library GL stack beneath the EasyGL family — How meta-gl and easy-gl split loading, typed calls, ownership and failure beneath CNA's five GL identities: revisions CNA needs, feature gating, per-thread state, context loss, tests and build inheritance.
- Module boundaries: cycles, umbrellas and the gates that enforce them — Why CNA keeps three static-archive cycles, what the CNA, CnaExt and BuildConfig compositions carry, what each boundary gate and the fifteen module probes check, when they run and what a violation looks like.
- sharp-runtime components and how CNA consumes them — How sharp-runtime is cut into CMake components, how an include maps to a component and link closure, and how CNA 009d40f5 selects, links and instruments the components it needs.
- The CNA ecosystem: siblings, references and consumers — Which repositories CNA builds against, audits against and is consumed by, when CMake needs each sibling, where each sibling draws its boundary, and which revision each statement refers to.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-194: The SDL2-only configuration guard recommends VULKAN, which the SDL2 platform cannot run — Sdl2OnlyConfiguration.cmake's error text lists VULKAN among the SDL-independent renderers to choose, but Sdl2Platform::GetVulkanSurface returns null, so a VULKAN build on SDL2 can only fail later at GraphicsDevice constr
- CNA-BUG-195: The SDL2-only guard inspects only CNA_GRAPHICS_RENDERER, so a multi-renderer list containing an SDL3-linked family is not refused — Sdl2OnlyConfiguration.cmake checks the scalar CNA_GRAPHICS_RENDERER but never the CNA_GRAPHICS_RENDERERS list, so an SDL2-only build whose list includes an SDL3-linked family passes the guard meant to keep two SDL majors
- CNA-BUG-202: In a multi-renderer build, non-default renderers' example CTests are registered but run under the default renderer — The SDL_GPU, SOFTWARE, STUB and VULKAN example blocks are entered for a non-default member of CNA_GRAPHICS_RENDERERS, yet their registrations select no renderer, so each executable runs under the build default.
- CNA-VGAP-011: No test exercises PlatformSelection.cmake's reserved-identifier and unknown-name refusal paths — PlatformSelection.cmake fails configure for SDL12, EMSCRIPTEN, WIN32 off Windows, TERMINAL on Windows and unknown names, but the only platform-selection configure test, CnaWaylandPlatformSelection, covers Wayland alone.
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- User guide
- Building CNA: CMake options · Building CNA: presets · Platforms: the four axes · Runtime renderer selection
- Architecture
- CMake architecture
- Internals
- Renderer selection internals · Platform backends
- Maintainer workflow
- Fix a renderer bug · What to test after changing X · Blast radius and readiness
- Tests and validation
- Test architecture
- Reference
- CMake option index · Selection axes index