Configuring CNA per target: routes, toolchains and what a green build proves
Evidence basis: source-verified at the pinned commit. 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 from the root CMakeLists.txt, cmake/ThirdPartySDL.cmake, cmake/UnitTests.cmake, the two toolchain files, CMakePresets.json and the workflow files; no route was configured, built or run. The MinGW emulator gap and the i686 key are readings of the CMake logic.
One CNA source tree is configured along several routes: a native Linux build, native Windows with MSVC, a MinGW-w64 cross-build from Linux, macOS and iOS, the Android NDK and Emscripten. Each route has its own toolchain source, its own way of finding SDL and running tests, and its own ceiling on what a successful build can prove. This page is for anyone who configures CNA outside the default Linux tree or who has to judge what a green configure, build or test run on one route actually establishes. Option-by-option tables live on Building CNA; the configure order is traced on CMake architecture.
What every route shares
Every route starts at the same root CMakeLists.txt: cmake_minimum_required(VERSION 3.20), C++23 with extensions off, and the release identity. The identity is decided once, by project(CNA VERSION 0.1.0) plus the normal (deliberately non-cache) variable CNA_VERSION_PRERELEASE, and the first status line of any configure prints the joined string: CNA: version 0.1.0-alpha.1. The snapshot still reports the pre-release label of the last tag; the banner identifies the product version, not the commit, so it cannot tell a tagged checkout from this development snapshot. The generated header CNA/Version.hpp that exposes the same values to code is described on Releases: reading the product version.
No route checks the compiler version. What the tree requires is the language level (cxx_std_23 on the public build-configuration target) and, in practice, a standard library with <format>, because the always-built content modules use it. A minimum such as "GCC 12 or Clang 15" is therefore not something the build enforces or CI exercises; the compilers that CI's workflows name are GCC 14 on Ubuntu 24.04, AppleClang on macOS 14, MSVC on windows-latest, mingw-w64 GCC and Emscripten 6.0.3. Claims about clang-cl or MSVC point releases are not backed by any workflow.
The one build-wide number that is not the same on every route is the linkage layout. On native ELF with GNU or Clang and CMake 3.27 or newer, CNA_SHARED_LIBRARY defaults to ON and every executable links one libcna.so; on Windows, macOS, Android, iOS and Emscripten the framework stays a set of static archives. A statement such as "most framework pieces are static archives composed into the final consumer" is therefore true of every route except the default Linux one. The decision is taken before any target exists because the archives that go into the shared library need position-independent code.
Toolchains: two files in the tree, the rest from the SDKs
cmake/toolchains holds exactly two files: mingw-w64.cmake for Windows cross-builds and ios.cmake for iOS device and simulator builds. Android uses the NDK's own android.toolchain.cmake and Emscripten uses emcmake (or the Emscripten toolchain file); neither has a CNA preset that supplies the toolchain. That matters most for the web preset in CMakePresets.json: it names no toolchain file, so cmake --preset web on its own configures a native tree, and the preset's own description says to run it as emcmake cmake --preset web.
The MinGW file has two details that surprise people. It looks for x86_64-w64-mingw32-gcc and silently falls back to the i686 triple when that compiler is absent, while CMAKE_SYSTEM_PROCESSOR stays hard-coded to x86_64; because the persistent SDL install is keyed by system name and processor, a host with only the 32-bit compiler still produces a Windows-x86_64 key. And its header comment still says that pre-built Windows SDL packages must be on CMAKE_PREFIX_PATH, which is no longer how the build works: the vendored SDL is compiled for the Windows target at configure time into its own .sdl-prebuilt-Windows-x86_64 root. Trust cmake/ThirdPartySDL.cmake over the comment.
| Route | Toolchain source | Default renderer | SDL3 from the vendored build | How tests execute |
|---|---|---|---|---|
| Linux native | host compiler | OPENGLES3 (needs easy-gl and meta-gl) | shared .so | directly; window tests need a display |
| Windows, MSVC | developer environment | SDL_RENDERER | shared DLL, copied beside the executables | directly |
| Windows, MinGW cross | mingw-w64.cmake | SDL_RENDERER | shared DLL for the Windows target | through a Wine wrapper for four renderers only |
| macOS | host AppleClang | SDL_RENDERER | shared .dylib | directly |
| iOS | ios.cmake | SDL_RENDERER (the only allowed one) | static | no tests; a smoke app is final-linked |
| Android | NDK toolchain | SDL_RENDERER | shared .so | no route in CMake |
| Emscripten | emcmake | WEBGL2 | static | CnaTests.js under Node, browser suites separately |
Windows routes
Native MSVC
A native configure needs only the sharp-runtime sibling for the default SDL_RENDERER: SDL is built from the vendored submodules at configure time, so no pre-built SDL and no CMAKE_PREFIX_PATH are required. One step of that configure is specific to Windows: after the SDL install, ThirdPartySDL.cmake copies every DLL of the SDL install's bin directory into the top of the build tree. Windows resolves a DLL from the executable's own directory first and has no RPATH, so an executable linked against the import libraries cannot start without the DLL beside it; the loader stops the process before main() with status 0xC0000135, which looks nothing like a test failure. Every test executable is written to the build root, so one copy there serves them all, and it is made at configure time because that is when the DLLs are produced.
The private compiler policy target cna_project_options in modules/CMakeLists.txt carries the three MSVC-specific settings that the first native MSVC build needed: /utf-8 (the sources are UTF-8), /bigobj (a template-heavy serializer translation unit exceeds COFF's section limit with error C1128) and NOMINMAX on every Windows target (the SDK's min/max macros break std::max; the MinGW cross-builds never showed it because mingw-w64's libstdc++ defines NOMINMAX itself). Native MSVC is exercised only by the manual-dispatch Windows workflows.
MinGW-w64 cross-build and Wine
The cross-build produces Windows binaries on Linux; running them is a separate step. In a cross tree, cmake/UnitTests.cmake sets a CROSSCOMPILING_EMULATOR on CnaTests only when the default renderer is DIRECTX9 (scripts/run-wine-dxvk9.sh), DIRECTX11 (scripts/run-wine-dxvk.sh), DIRECTX12 (scripts/run-wine-vkd3d.sh) or when DIRECT2D is compiled in (scripts/run-wine-direct2d.sh). The property has to exist before gtest_discover_tests runs, because discovery executes the binary to list its cases. Each wrapper is invoked with an environment flag that skips its DXVK or vkd3d-proton presence gate, since listing tests or running ordinary unit tests never creates a graphics device. By reading, a cross tree whose default renderer is none of those four gets no emulator, so CTest would try to execute the Windows binary directly on the Linux host unless the host itself hands PE files to Wine.
The evidence ceiling of this route is explicit in CI: the automatic win32-cross job of platform-ci.yml cross-builds and runs under Wine only the Win32 platform harness in tools/platform/standalone_tests, not the engine, and the full-engine Direct3D, Direct2D and GDI builds are manual MSVC workflows. A green DXVK or vkd3d-proton run on a Linux GPU is evidence for that translation stack, not for native Windows drivers. The user-level recipe is Tutorial 103.
Android: a compile route, not a test route
There is no Android preset, workflow or CMake API-level check. The only Android build project in the repository is the packaged Devices demo, whose jni/CMakeLists.txt adds the CNA root as a subdirectory with CNA_BUILD_TESTS and CNA_BUILD_EXAMPLES forced OFF, reusing CNA's own .sdl-prebuilt-Android-aarch64 install instead of the second SDL copy the Android template would otherwise build. The Gradle side pins NDK 30.0.14904198, API 24 and arm64-v8a. The NDK path in any command is whatever is installed locally, not a requirement.
Two things are commonly written about this route that the tree does not bear out. A command ending in --target CNA cannot work, because CNA is an INTERFACE library with no sources; build a real target or the demo. And the claim that tests must be off because GoogleTest cannot be configured for the NDK is not something the CMake states: nothing refuses CNA_BUILD_TESTS=ON for Android, the minimal-link module probes are skipped there by design (their guard is NOT EMSCRIPTEN AND NOT ANDROID), and whether a test build links for Android was not established. A successful Android configure and compile proves that the platform-conditional code compiles for the NDK; it says nothing about packaging, an emulator or a device. See Tutorial 82.
Emscripten: Node and the browser are different claims
Under Emscripten the browser-only renderers (WEBGL1, WEBGL2, CANVAS, HTML_DOM, SVG_DOM) are admitted and the native GL profiles are refused; a native configure asking for a browser renderer stops with a message that tells you to use emcmake. The test executable is built too: UnitTests.cmake links CnaTests with -sEXIT_RUNTIME=1, because Emscripten otherwise keeps the JavaScript runtime alive after main() returns and node --experimental-wasm-stack-switching CnaTests.js would never exit, and with -sJSPI=1, so that loopback networking tests can yield to Node's event loop from inside C++ code. Both settings are scoped to CnaTests and change no application target.
That gives two distinct kinds of web evidence. Running CnaTests.js under Node proves that renderer-independent code compiles to WebAssembly and behaves there; Node has no DOM, no CanvasRenderingContext2D and no WebGL context, so it says nothing about a browser renderer. Browser behaviour is exercised separately: htmldom-ci.yml drives the HTML_DOM smoke, pixel, stress and dispose pages in headless Chromium through Playwright, and other browser renderers have script-driven runs under scripts (for example run-svgdom-browser-test.sh) that no workflow invokes. The emscripten-multi-renderer-ci.yml job builds and links one bundle containing four browser renderers and checks the JavaScript selection exports; it runs nothing in a browser. Earlier development runs recorded a Node pass of a renderer-agnostic suite on an older Emscripten; that is history for its own revision, not evidence for this snapshot. The user walk-through is Tutorial 81.
Apple routes in one paragraph
macOS builds natively with the default SDL_RENDERER or the macOS-only METAL; iOS goes through ios.cmake with CNA_IOS_SIMULATOR choosing the SDK, is restricted to SDL_RENDERER unless CNA_APPLE_ALLOW_UNVALIDATED_RENDERER=ON, links SDL statically (an embedded dylib would need separate embedding and signing inside the app bundle), and builds the smoke application instead of tests. Deployment floors of macOS 13.3 and iOS 16.3 are hard errors below them, and the deployment target is part of the SDL install key because SDL objects carry their own minimum-OS load command. Details are on Platforms: macOS and iOS.
Two build-speed settings that behave unexpectedly
The compiler launcher is replaced, not added. With CNA_USE_CCACHE=ON (the default) and ccache on the path, the root file sets both CMAKE_C_COMPILER_LAUNCHER and CMAKE_CXX_COMPILER_LAUNCHER to cmake -E env CCACHE_BASEDIR=<dir> ccache. It overwrites a launcher that is unset, empty, the bare word ccache or the path of the ccache it found; any other launcher is left alone. So passing -DCMAKE_CXX_COMPILER_LAUNCHER=ccache is harmless but redundant, and a different wrapper (a distributed compiler, a timing tool) survives. The launcher is set before add_subdirectory of sharp-runtime, so the sibling's objects are cached too, and CNA_CCACHE_BASEDIR defaults to an exported CCACHE_BASEDIR when the shell has one, otherwise the directory above the source tree, so one translation unit hashes the same from every build tree under it.
The vendored SDL build has its own job limit. CNA_MAX_VENDORED_BUILD_JOBS (default 2, validated as a positive integer) bounds the cmake --build --parallel of the configure-time SDL sub-builds. It exists because those sub-builds are separate CMake invocations: a bare --parallel there defers to the build tool's unrestricted default, so a -j2 given to the parent build would not limit them.
What each verification step proves
CNA ships no single playable demo as its proof of a working build; it offers a ladder of steps, and each rung proves something narrower than it looks.
- Configure succeeded. The selections are mutually admissible on this host and every required sibling, submodule and package was found. Read the status lines before anything else: the resolved platform and audio values,
CNA: renderer set -- ..., whether video was enabled, whether SDL was configured at all, and whether the SDL install was reused or rebuilt (the prefixes are listed on Build system: reading the configure log). - A target built.
cmake --build build --target cna_demo_2dproves the link closure of one executable for the selected renderer and platform.CNAitself is not a target. - A program ran.
./cna_demo_2d --smoke 6, run from the build directory so it finds its copied content, runs six frames and exits. It proves start-up, a few frames and shutdown on this host, not pixels. - The unit corpus ran. Running the
CnaTestsbinary directly (thetestspreset's own advice) exercises the GoogleTest corpus compiled into this configuration. Its population depends on the configuration, so its count is a property of the tree, not of CNA. - Selected registrations ran.
ctest -L <label>or-R <regex>after a full build runs the standalone programs of a renderer or subsystem. An unfilteredctestafter building onlyCnaTestsreports every unbuilt standalone program as a failure.
None of these rungs is an oracle comparison. Which tool answers which question, and why a skipped test is not a pass, is on Tests, examples, presets and tools. On the CI side, all 20 workflow files can be dispatched by hand and 18 also have a push trigger: 17 on pushes and pull requests to next, develop and main, and content-pipeline-windows-ci.yml only on pushes to its own branch. A green badge is evidence for the route that workflow engages, not for every route on this page; the per-workflow table is Test architecture: what CI covers.
Evidence and limits
Everything above was read from CMakeLists.txt, cmake/ThirdPartySDL.cmake, cmake/UnitTests.cmake, the two toolchain files, CMakePresets.json and the workflow files at snapshot 009d40f5; no route was configured, built or run for this page. The statements about what happens without an emulator in a MinGW tree, and about the i686 fallback's effect on the SDL install key, are readings of the CMake logic, not observed failures.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Building CNA: cross-compiling · Platforms: Windows
- Architecture
- CMake architecture: root configure order
- Maintainer workflow
- I need to change build configuration
- Tests and validation
- Test architecture: what CI covers
- Reference
- CMake option index