Dependency acquisition: submodules, siblings, fetched pins and host packages
Evidence basis: source-verified at the pinned commit. Claims on this page were checked by reading the CNA source at commit 3b43bd86; unless a sentence says otherwise, nothing here was built or executed. Read from .gitmodules, the cmake/ThirdParty*.cmake files, Fna3dSourcePin.cmake, Fna3dSourceCheck.cmake, VendoredSourceCheck.cmake, SharpRuntimeConsumption.cmake, RendererSelection.cmake, modules/CMakeLists.txt, modules/c-api/CMakeLists.txt and CMakePresets.json; nothing was configured, fetched, built or installed.
A successful CNA configure can involve checked-out submodules, sibling repositories, sources and binaries fetched at configure time, and packages and SDKs found on the host. Calling all of that "vendored dependencies" hides the most common reproducibility failures, because each mechanism has a different pin, a different offline story and a different way of failing. This page classifies them at snapshot 3b43bd86, explains how they compose, and says what each one does to the build when it is present, absent or different. Pins and override variables are listed on Source ownership: third-party boundaries; the SDL install's manifest mechanics on CMake architecture.
Five ways outside code enters a build
| Mechanism | At this snapshot | Pinned by | Reproducibility boundary |
|---|---|---|---|
| Git submodules | third_party/SDL, third_party/SDL_mixer, third_party/draco (Draco 1.5.7), vendor/googletest and the avatar catalog modules/gamer-services/assets/avatars (the cna-avatar-assets repository) | the gitlink commits in CNA's own tree; for SDL and SDL_mixer the configure also checks that the checkout is that commit | A ZIP or tarball export contains none of them; each has a configure guard that names the directory and the fix |
| Sources vendored in the tree | third_party/enet (upstream ENet master 5a9c537, unmodified), third_party/stb, third_party/dr_libs, third_party/cgltf | being committed | Always present, but compiled only by the module and option that need them (ENet only with networking, dr_libs and stb_vorbis for the ALSA mixer) |
| Sibling checkouts | ../sharp-runtime (every build), ../easy-gl with ../meta-gl (the three GL identities) | a compatible declared version per sibling (the stated version or a later patch release of the same minor), checked at configure time by DependencyVersions.cmake (sharp-runtime 0.1.0, easy-gl 0.1.1, meta-gl 0.4.1); CI checks out the sibling branch that matches the run's branch | Whatever commit the caller checked out; CNA compares no SHA, and an incompatible declared version fails the configure (a warning with -DCNA_CHECK_DEPENDENCY_VERSIONS=OFF) |
| Fetched at configure time | FNA3D with MojoShader (CNA's forks of both), SDL_shadercross with SPIRV-Cross, and the wgpu-native binary release | cache variables in the cmake/ThirdParty*.cmake files | Network access on the first configure that selects them, unless an offline source directory or root is given |
| Host packages and SDKs | FFmpeg, libzstd, FreeType, zlib, OpenGL, the X11 and Wayland development packages (for SDL's video drivers), ALSA headers, Threads, the Windows and Apple SDKs | nothing | Can change which files compile, or whether the configure is admissible at all |
There is no in-tree copy of wgpu-native: the native WebGPU renderer uses a downloaded official binary release, and a browser build uses Emscripten's own port. vendor/ holds only GoogleTest.
Mechanisms compose, so a CNA commit is not a build identity
The categories nest. FNA3D is fetched from CNA's fork at a pinned commit and brings MojoShader, also CNA's fork, as its own submodule; CNA's fixes to both are commits on the forks, so the fetched checkout is built as it is and nothing patches it at configure time. easy-gl is a sibling that itself expects ../meta-gl as a sibling. The identity of one build is therefore the CNA commit, plus the submodule commits it records, plus sibling commits that a build checks only by declared version (CNA's changelog records the commits a release was verified against, not those of a snapshot build), plus fetch pins that live in overridable cache variables, plus host package versions that nothing records, plus the manifest stored beside the persistent SDL install. A lock file for CNA's own Git tree alone does not reproduce a build.
Submodules: initialise, do not recurse
The instruction that the CMake gives is git submodule update --init, deliberately not --recursive. ThirdPartySDL.cmake explains why in the comment above its guard: recursion also clones the nested codec submodules of SDL_mixer and Draco, none of which CNA's sub-build options enable, and it measured six to seven times slower for no compiled input. The README agrees at this snapshot: --recursive is not needed.
Each missing submodule is caught by its own configure guard, and only when the option that needs it is on: the two SDL trees when SDL is configured at all and CNA_USE_SYSTEM_SDL is OFF; vendor/googletest while CNA_BUILD_TESTS is ON (its message says that a ZIP or tarball export cannot contain submodule content); third_party/draco while CNA_ENABLE_DRACO is ON and no system Draco is requested; the avatar catalog while CNA_ENABLE_GAMER_SERVICES is ON (its message adds that a source archive contains no submodules and that a build without Gamer Services does not need the catalog). The one place where a recursive update is required is outside CNA's tree: an offline FNA3D checkout supplied through FETCHCONTENT_SOURCE_DIR_FNA3D needs its own MojoShader submodule, and the configure refuses it if that submodule is missing.
Present is not the same as current. A git pull that moves a gitlink without git submodule update leaves the old SDL sources in place, and the vendored route would build them. Every configure therefore compares third_party/SDL and third_party/SDL_mixer with the revisions CNA pins (VendoredSourceCheck.cmake, read-only): an initialized submodule must be at the gitlink commit, and copied sources or a source archive are compared by Git tree id with the tree recorded in cmake/VendoredSources.cmake. A mismatch stops the configure with the repair; local changes are only a warning, a mode-only mismatch on Windows is a warning, and without Git the check prints a status line and goes on. CNA_VENDORED_SOURCE_CHECK (ON by default, WARN, OFF) relaxes it for building other sources on purpose.
For a distributable source snapshot, either ship the exact submodule trees or choose the system-SDL route deliberately and provide GoogleTest another way (or build without tests). Silently building against whatever SDL happens to be installed changes the dependency claim of the result.
The configure-time SDL build and what it implies
With the default vendored route, cmake/ThirdPartySDL.cmake configures, builds and installs SDL3 and SDL3_mixer during CNA's configure step (there is no SDL3_image: CNA decodes images itself), into a persistent prefix outside every build tree, and then consumes them through their exported CMake packages. The operational consequences:
- Deleting or cleaning a CNA build directory does not rebuild SDL; a changed vendored source, patch, sub-build argument, toolchain file, compiler or toolchain-decided setting does, because the install is reused only while its recorded manifest matches. Each build tree also watches the install record and reconfigures when another tree has rebuilt the shared install; CNA records as a limitation that a tree with a different manifest can still rebuild it under a build that is running, so such a tree should get its own
CNA_SDL_PREBUILT_ROOT. - The prefix is keyed by target system and processor (plus Wayland, simulator and deployment-target suffixes where they apply), so a cross-build cannot overwrite the native install or reuse it.
- A configure failure can be a nested dependency failure rather than a CNA CMake error. Such failures read
CNA: SDL3 cmake configure failed (exit code N),... build failed,... install failedor... installed, but <library> is not there; the cause is in the sub-build's own output. - If a parent project has already created both targets
SDL3::SDL3andSDL3_mixer::SDL3_mixerbefore adding CNA, the vendored setup returns immediately and uses them. WithCNA_USE_SYSTEM_SDL=ONit instead finds the installed SDL3 and SDL3_mixer packages. WithCNA_ENABLE_SDL=OFFnone of this runs.
The sub-builds prune optional codecs. SDL_mixer is configured with the external codec libraries switched off (SDLMIXER_MP3_MPG123, SDLMIXER_OPUS, SDLMIXER_MIDI_FLUIDSYNTH, SDLMIXER_WAVPACK, SDLMIXER_FLAC_LIBFLAC, SDLMIXER_VORBIS_VORBISFILE, SDLMIXER_VORBIS_TREMOR, SDLMIXER_GME, SDLMIXER_MOD_XMP all OFF), CNA's audio and media behaviour on the SDL3 audio route has to be read against that concrete mixer build, not against everything SDL_mixer can do in principle; which formats SDL_mixer still decodes with its built-in decoders in this configuration was not established here. The ALSA route uses CNA's own mixer and its own decoder set instead (see Audio). The small default parallelism of these sub-builds is explained on Configuring CNA per target.
Host packages can change what compiles
"Dependency found" is not merely a link flag. At this snapshot optional host dependencies produce three different outcomes, and it helps to name which one a given package causes.
1. A backend is omitted and the API refuses at run time
FFmpeg is the example. modules/CMakeLists.txt computes CNA_FFMPEG_AVAILABLE from CNA_ENABLE_VIDEO (OFF, AUTO, ON) and four pkg-config modules: libavcodec, libavformat, libavutil and libswresample. libswscale is neither included nor linked; the conversion of decoded YUV frames to RGBA is CNA's own code in the FFmpeg backend. When the flag is off, the video-ffmpeg module is not entered and the media module compiles two small fallback units instead, so Video, VideoPlayer and the XNB video reader still exist and link, and playback throws System::NotSupportedException. The flag is also exported to the root scope, because the unit-test registration keys its real-decoder fixture suites on it; the comment there notes that an unset variable would silently drop those suites on capable hosts.
Cross-builds never probe the build host. On MinGW, Windows, Emscripten, Android and iOS the FFmpeg probe is skipped (AUTO falls back to the no-video backend, ON is a configure error), so a cross-configure cannot import the host's native pkg-config headers and libraries into a target build. The optional zstd support for compressed .cnb chunks follows the same rule more narrowly: it skips pkg-config whenever CMAKE_CROSSCOMPILING is set, because an Android configure once found the host's x86_64 libzstd while compiling ARM64 objects, and falls back to find_path/find_library, which respect the cross toolchain's root.
2. The feature compiles, and a format is refused at run time
Draco is the example. Without it the glTF importer still exists, and a primitive that uses KHR_draco_mesh_compression is refused with a named error at import time. The default is the repository's own gitlink at Draco 1.5.7 rather than a host package, so that conformance does not depend on which distribution version happens to be installed; CNA_USE_SYSTEM_DRACO=ON is an explicit packager's escape hatch. The default is ON everywhere except Emscripten, and modules/CMakeLists.txt records why: the pinned Draco demands an EMSCRIPTEN environment variable that current emsdk no longer exports (CNA now derives it from the toolchain), one Draco source uses std::all_of without including <algorithm>, which libstdc++ tolerates and libc++ does not (CNA force-includes the header into that one Draco target on Clang, leaving the submodule byte-identical), and Draco's own --whole-archive self-link makes wasm-ld report the whole library as duplicate symbols. Opting in on the web is therefore expected to fail at link time until the pin moves.
3. The configuration is refused
Some selections cannot be satisfied without a package and stop the configure with a message: a retired CNA_PLATFORM value (WIN32, X11, WAYLAND) names SDL3 instead, CNA_AUDIO_PLATFORM=ALSA needs the ALSA headers, CNA_CNB_ZSTD=ON needs libzstd, CNA_ENABLE_FONT_PIPELINE=ON needs FreeType, and CNA_ENABLE_VIDEO=ON fails on a target outside the FFmpeg integration. These three outcomes (source omitted with a run-time refusal, format rejected at run time, configuration refused) are what a report should distinguish instead of writing "optional".
Sibling repositories are part of the workspace contract
sharp-runtime
sharp-runtime is required by every build. CNA_SHARP_RUNTIME_ROOT defaults to ../sharp-runtime; when the directory has no CMakeLists.txt the root configure stops with a message explaining that it is a separate checkout, not a submodule, and printing a clone command. cmake/SharpRuntimeConsumption.cmake then detects the checkout's shape: if the target SharpRuntime::Core.Base exists the checkout is modular and each CNA module links exactly the components it names through cna_link_sharp_runtime(); otherwise it is treated as the older monolithic shape and every module links the single archive, a fallback kept for old checkouts. CNA's default component closure is Core.Base, IO, Collections.Core, Collections.ObjectModel, Runtime, Threading, Text, Globalization, ComponentModel, Storage, Security.Cryptography, Xml and Resources, plus Xml.Serialization on non-Windows targets (on Windows that component would drag in a sibling source that includes <poll.h>, which neither MinGW nor MSVC provides; a later sharp-runtime commit, 88c12f15, guards that include, but CNA's rule at this snapshot still excludes the component on Windows). The root file merges this closure into an existing cache value, so a build directory configured before a component joined the closure picks it up on the next reconfigure instead of failing at link time. A checkout too old to have Resources or Xml.Serialization (sharp-runtime's main and develop of late August 2026, for example) fails inside sharp-runtime's own configure, which rejects the requested component with “Unknown Sharp Runtime component”; this snapshot pairs with sharp-runtime's next branch, and the 0.1.0 release with its v0.1.0 tag.
No part of CNA checks which sharp-runtime commit it got; since 0.1.0 it checks only the version the checkout declares (0.1.0 or a later 0.1 patch release; a pre-release of 0.1.0 is refused), and the exact commits each release was verified against stay in CHANGELOG.md. For this snapshot, which is on CNA's next branch, sharp-runtime's next branch is the matching checkout (the v0.1.0 tag matches the 0.1.0 release). At this snapshot every workflow chooses the sibling branch the same way: scripts/ci/sibling_branch.sh, called directly or through clone_siblings.sh, picks the first branch the sibling actually has among the pushed branch and the pull request's source and target, then next, then develop; no workflow pins an older sibling commit.
easy-gl and meta-gl
The three EasyGL identities need easy-gl, and easy-gl in turn expects ../meta-gl beside itself. Like sharp-runtime, the easy-gl location is a cache path: CNA_EASYGL_ROOT defaults to ../easy-gl and can point at a checkout elsewhere, for example from a worktree nested where it has no sibling of its own. cmake/RendererSelection.cmake tests that path and adds it once, even when several GL identities share a multi-renderer build. easy-gl (with meta-gl) is the only renderer dependency that is a sibling repository, and the configure checks their declared versions (easy-gl 0.1.1, meta-gl 0.4.1) the same way it checks sharp-runtime's. A checkout that clones only CNA and sharp-runtime can therefore configure SDL_GPU, SOFTWARE or HEADLESS but not the Linux default, OPENGLES3.
Do not paper over a missing sibling by copying its sources into CNA's tree or build directory. A sibling's own commit, tests, public targets and other consumers are part of any claim made about a build that uses it; a copy erases all of that. The roles and revisions of the siblings are on The CNA ecosystem.
Renderer dependencies are deliberately asymmetric
| Renderer | How its dependency arrives | Offline or override |
|---|---|---|
WEBGPU (native) | the official wgpu-native v29.0.1.1 binary release for the target (x86_64 or aarch64 on Linux, macOS and MSVC Windows; x86_64 only for MinGW), downloaded into the build tree's _deps directory and SHA-256-verified before extraction, so unlike the SDL install it is per build tree | CNA_WEBGPU_ROOT with CNA_WEBGPU_AUTO_DOWNLOAD=OFF |
WEBGPU (Emscripten) | Emscripten's emdawnwebgpu port: the browser supplies WebGPU, nothing is downloaded | none needed |
FNA3D and every compiled-effects option | CNA's fork libcna/FNA3D fetched at commit 10f650b9 of its cna branch, with its MojoShader submodule (CNA's fork libcna/mojoshader, 03b686fe), built as it is and linked statically; every configure compares the checkout with the pin (Fna3dSourceCheck.cmake) and refuses a FetchContent checkout that has drifted, unless CNA_FNA3D_ALLOW_SOURCE_DRIFT=ON | FETCHCONTENT_SOURCE_DIR_FNA3D pointing at a clone of the fork at the pin with its MojoShader submodule initialized; such a checkout is built as it is and a difference from the pin is only reported. CNA_FNA3D_GIT_REPOSITORY and CNA_FNA3D_GIT_TAG move as one pair. A build tree whose _deps/fna3d-src still holds an older, locally patched checkout stops; deleting _deps/fna3d-* fetches the pin afresh |
SDL_GPU | SDL3 itself, plus SDL_shadercross and SPIRV-Cross when CNA_SDL_GPU_SHADERCROSS is on (default on Windows and Apple); CNA's one build-integration patch to SDL_shadercross is applied only to the clone FetchContent made for the build tree, or to a staged copy of a supplied checkout, never to a developer's own checkout | the CNA_SDL_SHADERCROSS_GIT_* and CNA_SPIRV_CROSS_GIT_* variables |
DIRECTX9 | the Windows SDK of the target toolchain (MinGW-w64 or MSVC) | the toolchain |
| The three GL identities | sibling checkouts (above) | none for the path |
Accordingly, "every identity CMake accepts" is not "every renderer dependency is available on this host". A name can pass the identity check and still be refused a few lines later by its SDK gate, its platform gate or a missing sibling; how the selection file orders those checks is on Renderer selection internals.
How CNA itself is consumed
By default the C++ framework is a build-tree product: a game adds the checkout with add_subdirectory (with the same siblings next to it), usually with CNA_BUILD_TESTS and CNA_BUILD_EXAMPLES off, and links CNA or narrower aliases. Several details exist so that this works: the legacy-root guard, the generated CNA/Version.hpp and the graphics module's stb include root are anchored on CNA_SOURCE_DIR/CNA_BINARY_DIR rather than CMAKE_SOURCE_DIR/CMAKE_BINARY_DIR, which in a subproject name the consumer's root; a parent may supply the two SDL targets itself; and the packaged Android demo adds the CNA root this way and reuses CNA's own SDL install. CNA is a CMake target and CNA::Math and the others are aliases; neither implies a file called libCNA. On native ELF builds with CNA_SHARED_LIBRARY the runtime file is libcna.so (target cna_shared, alias CNA::Shared); elsewhere the modules are static archives composed into the final executable.
Two install surfaces are opt-in. CNA_INSTALL_CXX=ON (OFF by default, cmake/InstallCxx.cmake) installs a relocatable C++ module SDK: the target cna_cpp_sdk builds it, the install components SharpRuntime and CNACpp carry sharp-runtime's closure and CNA's module archives and headers, and a consumer finds it with find_package(CNA CONFIG REQUIRED COMPONENTS ...). Its package version is the C ABI version (0.50.0), and CNA records only native Linux shared and static SDKs as validated; the user-level description is on Building: the installed C++ SDK. The optional C API is the other install surface, and it is deliberately narrower. Its rules install the shared CNA::CApi library (and a static CNA::CApiStatic archive on non-Apple Unix builds when Python 3 is available), the C headers, CNAConfig.cmake, CNACTargets.cmake and a package-version file, together with the vendored SDL shared libraries it needs at run time (not SDL's own CMake or pkg-config files, and not FFmpeg, which stays a system dependency). The package version is read from CNA_ABI_VERSION_* in abi.h, so it is the C ABI version, not the product version, and its compatibility rule is SameMajorVersion. An external consumer test (CApi_InstalledConsumer, building hello_cna.c through find_package(CNA CONFIG)) exists in the source; no workflow builds the C API library, so its status is described on C API: intended build. Neither package removes the sibling contract for source builds.
Evidence and limits
Read at c6dec91f and re-checked at 3b43bd86, where the FNA3D, ENet, vendored-source and avatar-catalog statements were rewritten from cmake/ThirdPartyFNA3D.cmake, cmake/Fna3dSourceCheck.cmake, cmake/ThirdPartyENet.cmake, cmake/VendoredSourceCheck.cmake and modules/gamer-services/CMakeLists.txt; from .gitmodules, cmake/ThirdPartySDL.cmake, the other cmake/ThirdParty*.cmake files, cmake/SharpRuntimeConsumption.cmake, cmake/RendererSelection.cmake, modules/CMakeLists.txt, modules/c-api/CMakeLists.txt and CMakePresets.json. Nothing was configured, fetched, built or installed. The effect of a sharp-runtime branch without the required components is read from the target resolution, not reproduced; the six-to-sevenfold recursion cost is CNA's own recorded measurement.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Architecture
- CMake architecture: third-party dependencies
- Maintainer workflow
- Change build configuration: third-party sources
- Reference
- CMake option index
- Deep dives
- The CNA ecosystem · Configuring CNA per target