Tutorial 100: Shipping Your CNA Game
What you’ll learn
- Producing a stripped release build and bundling assets.
- Packaging per platform: Windows installer, Linux AppImage/Flatpak, web, and Play Store.
- Meeting the licence obligations of Ms-PL, SDL3 and the other dependencies.
- The per-platform gaps you must design around before you ship.
- A final pre-release checklist.
Before you start — Tutorial 20: Building and Running Your Game (the release build) and Tutorial 80: Cross-Platform Build Guide (per-platform packaging starts from those toolchains).
Platform gaps to ship around
Four of these are easy to miss until a player reports them. Settle each one before you cut a release build.
| Gap | Where it bites | What to do |
|---|---|---|
| Web saves live in the browser’s IndexedDB | Emscripten builds only; threaded builds in particular | CNA mounts IndexedDB-backed storage for StorageDevice and restores it before main(), so saves survive a reload. A threaded build uses WasmFS by default, which cannot host that storage, and StorageDevice then reports it unavailable: configure -DCNA_EMSCRIPTEN_USE_WASMFS=OFF if a threaded web build needs saves. Players can clear a site’s storage, so do not treat a browser save as more durable than a desktop one. See Tutorial 81 and Tutorial 124. |
| No video | Windows, Web, Android and iOS | FFmpeg is never built for those targets. Video/VideoPlayer still compile and link there, but opening a Video or calling VideoPlayer::Play throws System::NotSupportedException. Guard it with #ifdef CNA_VIDEO_AVAILABLE (defined only when the FFmpeg backend is built in), or drop cutscenes from those targets. |
| FFmpeg is optional, and linked only when found | Linux and macOS builds | CNA_ENABLE_VIDEO defaults to AUTO: FFmpeg is linked only when its four development packages were found at configure time (or forced with ON; OFF skips it). If yours was, the shipped binary links FFmpeg runtime libraries, so bundle them (AppImage handles this) and account for their licences. If you do not need video, configure with -DCNA_ENABLE_VIDEO=OFF and ship without them. |
Game lifetime on the web (changed since alpha.1) |
Emscripten builds only | alpha.1 required a heap-allocated Game; a stack-allocated one was silently corrupted. In this snapshot Game::Run() suspends through Asyncify and returns normally, so a local Game is fine — provided your final executable links CNA::EmscriptenAsyncify (see Tutorial 81). If you ship against the alpha.1 tag, allocate with new/std::make_unique. |
Two more worth knowing before you commit to a target. iOS support is narrow and experimental: the Apple workflow is configured to final-link an SDL_RENDERER device app and launch a one-frame simulator smoke path, but does not establish physical-device or pixel correctness; tvOS is unsupported. Compiled effects are renderer-qualified: XNA/FNA D3D9 Effect Framework bytecode works on FNA3D and, when the matching *_COMPILED_EFFECTS option is switched on, on EasyGL-family, Vulkan, WebGPU, Software, DirectX 9, DirectX 11, Metal and SDL_GPU builds (a default configure enables it on FNA3D only). Other formats or renderers need ShaderEffect in the active renderer's language (see Tutorial 52).
Release CMake build
# Release build with maximum optimization
cmake -S . -B build-release \
-DCMAKE_BUILD_TYPE=Release \
-DCNA_GRAPHICS_RENDERER=OPENGLES3 \
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON # LTO
cmake --build build-release
Do not pass --target CNA. CNA is an add_library(CNA INTERFACE)
umbrella with no sources of its own, so it is not a buildable target. Build your own game target, or the whole
build directory.
The C++ framework is still consumed from source. It does not publish a general installed C++
package for games; consumers normally add_subdirectory the CNA tree. The exception is the
experimental CNA_BUILD_C_API surface (which requires CNA_ENABLE_NET=ON), whose source declares a CNACApi install component and
CNA::CApi/CNA::CApiStatic targets. No CI job builds that library and its release gate reports it as not ready, so it is not a shippable package. The CPack configuration later on this page packages
your game, not CNA.
Pick a renderer deliberately. A default build compiles one; CNA_GRAPHICS_RENDERERS
can opt into a compatible set with pre-device runtime selection. The value must be one of the 25 current
identities — a name outside them fails configuration by name, and old spellings such as EASYGL, D3D9, D3D11,
DX3 and ASCII are among the values it rejects. See
Tutorial 72.
Know what your binary loads. At this snapshot the vendored SDL3, SDL3_image and SDL3_mixer are shared libraries on Linux, macOS, Windows and Android, and on native Linux with CMake 3.27 or newer the engine itself is a shared libcna.so (CNA_SHARED_LIBRARY, on by default). CNA has no install() rules for any of them. Run ldd (Linux) or an equivalent on your release executable and ship every non-system library it lists, or configure with -DCNA_SHARED_LIBRARY=OFF to link the engine statically. The CMake install(TARGETS MyGame) steps below install your executable only.
Stripping debug symbols
# Linux: strip symbols to reduce binary size
strip --strip-unneeded build-release/MyGame
# Keep debug symbols separately for crash reports:
objcopy --only-keep-debug build-release/MyGame build-release/MyGame.debug
strip --strip-unneeded build-release/MyGame
objcopy --add-gnu-debuglink=MyGame.debug build-release/MyGame
Asset bundling
Place all game assets in an assets/ directory alongside the executable. For web
builds, preload via Emscripten. For compressed bundles, use a simple ZIP archive (extract at
first run) or a custom pack format.
# Copy assets to build output directory
add_custom_command(TARGET MyGame POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/assets
$<TARGET_FILE_DIR:MyGame>/assets
COMMENT "Copying assets"
)
Windows installer (NSIS/WiX)
# CPack NSIS installer (Windows)
set(CPACK_GENERATOR "NSIS")
set(CPACK_PACKAGE_NAME "MyGame")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_VENDOR "My Studio")
set(CPACK_NSIS_DISPLAY_NAME "My Game")
set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES64")
set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON)
set(CPACK_NSIS_MUI_ICON "${CMAKE_SOURCE_DIR}/icon.ico")
install(TARGETS MyGame DESTINATION .)
install(DIRECTORY assets DESTINATION .)
include(CPack)
Linux AppImage/Flatpak
# Create AppImage with linuxdeploy
wget https://github.com/linuxdeploy/linuxdeploy/releases/latest/download/linuxdeploy-x86_64.AppImage
chmod +x linuxdeploy-x86_64.AppImage
./linuxdeploy-x86_64.AppImage \
--appdir AppDir \
--executable build-release/MyGame \
--desktop-file mygame.desktop \
--icon-file mygame.png \
--output appimage
# mygame.desktop
[Desktop Entry]
Type=Application
Name=My Game
Exec=MyGame
Icon=mygame
Categories=Game;
Web deployment (GitHub Pages / itch.io)
# Build for web (emsdk 6.0.3 is the version CNA's CI pins). OPENGLES3 is NOT valid
# here -- it is gated to non-Emscripten targets. The Emscripten renderer is
# WEBGL2 (default); WEBGPU also has an experimental browser route.
emcmake cmake -S . -B build-wasm \
-DCNA_GRAPHICS_RENDERER=WEBGL2 -DCMAKE_BUILD_TYPE=Release
cmake --build build-wasm
# CNA also ships a "web" preset (it builds CNA's cna_house3d_demo; it names no
# toolchain file, so run it through emcmake):
# emcmake cmake --preset web
# Deploy to GitHub Pages (gh-pages branch):
git checkout gh-pages
cp build-wasm/MyGame.{html,js,wasm,data} docs/
git add docs/
git commit -m "Deploy web build"
git push
# Or upload to itch.io using butler:
butler push docs/ yourstudio/mygame:html5
Android Play Store
Build and sign a release APK:
# 1. Build release APK
cd android/
./gradlew assembleRelease
# 2. Sign with release keystore
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \
-keystore my-release-key.keystore \
app/build/outputs/apk/release/app-release-unsigned.apk \
my-alias
# 3. Align with zipalign
zipalign -v 4 app-release-unsigned.apk my-game-release.apk
# 4. Upload to Play Console via fastlane or the web UI
License compliance (Ms-PL + SDL3 + dependencies)
CNA is licensed under the Microsoft Public License (Ms-PL). Your shipped game must include:
LICENSEfile from the CNA repository (Ms-PL)- SDL3 license: zlib license (permissive, requires attribution in documentation)
- SDL3_image: zlib license
- SDL3_mixer: zlib license
- If using Vulkan: the Vulkan SDK components are Apache 2.0
- Depending on the options you built with, code CNA itself vendors or fetches: Draco (Apache 2.0, when
CNA_ENABLE_DRACOis on, which is the default except under Emscripten), ENet (MIT, whenCNA_ENABLE_NETis on, the default),stb_vorbisanddr_libs(public domain or MIT, in the ALSA audio backend’s own mixer), FNA3D and MojoShader (zlib, only for theFNA3Drenderer),wgpu-native(Apache 2.0 / MIT, only forWEBGPU) - If you enabled FFmpeg video: FFmpeg’s own licence, which depends on how the FFmpeg you linked was configured (LGPL or GPL)
- If using Box2D: MIT license
- If using Bullet: zlib license
CNA’s own THIRD_PARTY_NOTICES.md records most of what it vendors or fetches and under which licence, but at the audited snapshot it does not list cgltf, stb_image/stb_image_write, ENet or dr_libs (CNA-BUG-045); cgltf and the stb headers are compiled into the content and graphics modules, so take those licence texts from third_party/ yourself where they exist, and check the file against the options you built with. Nothing above is legal advice. Include a LICENSES.txt in your
distribution:
CNA -- Microsoft Public License (Ms-PL)
SDL3 -- zlib License, Copyright (C) 1997-2024 Sam Lantinga
SDL3_image -- zlib License
SDL3_mixer -- zlib License
[add any other dependencies]
Final checklist
CPack CMakeLists.txt for multi-platform packaging:
cmake_minimum_required(VERSION 3.20)
project(MyGame CXX)
set(CMAKE_CXX_STANDARD 23)
# ... your normal target setup ...
# ---- CPack configuration ----
set(CPACK_PACKAGE_NAME "MyGame")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_VENDOR "My Studio")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "An awesome CNA game")
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSES.txt")
if(WIN32)
set(CPACK_GENERATOR "NSIS;ZIP")
set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES64")
set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON)
elseif(APPLE)
set(CPACK_GENERATOR "DragNDrop")
elseif(UNIX)
set(CPACK_GENERATOR "TGZ;DEB")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "you@example.com")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libgl1")
endif()
install(TARGETS MyGame DESTINATION bin)
install(DIRECTORY assets DESTINATION bin)
install(FILES LICENSES.txt DESTINATION .)
include(CPack)
# Build package: cmake --build build-release --target package
CNA's own CI will not have caught every shipping environment for you. This snapshot contains 18 workflow files (16 of them run automatically) spanning general tests, focused subsystems, an Emscripten bundle build, Apple/Metal, a platform-implementation matrix (SDL3 on X11 and on Wayland, an SDL-free headless build, terminal and headless), runtime multi-renderer selection and five declared C API gates. The Windows Direct3D and SDL3-on-Windows lanes are manual, no workflow builds the Android target or the C API library, and the Linux GPU tests run on Mesa software drivers under Xvfb, so the matrix is not exhaustive across 14 identities, real drivers, Android devices or physical iOS hardware. Test on the platforms you actually ship to.
Shipping checklist:
- Release build compiled with -O2/-O3 and LTO enabled
- Debug symbols stripped from release binary (separate .debug file kept)
- All assets copied to distribution directory
- CNA's test suite run on the target platform — this snapshot contains 813 C++ test source files and 11,380 statically discoverable GoogleTest-family definitions, but the compiled and CTest-registered subset depends on the complete build configuration. Run it and read the output; do not assume a pass rate.
- Game runs without crash on fresh OS install (no missing DLLs / .so files)
- Audio tested (SoundEffect, background music) with the audio implementation you ship (
SDL3andALSAprovide a mixer;NULLdoes not play XNA audio). Note.m4a/.aacare unplayable — neither SDL3_mixer as configured nor CNA’s own mixer has an AAC decoder — and a WaveBank with XMA/WMA content logs to stderr and returnsnullptrrather than throwing, so the sound is simply missing. - Input tested (keyboard, controller if supported)
- Window resizing and fullscreen toggle tested
- LICENSES.txt included in distribution
- Web build tested in Chrome and Firefox — including a page reload, to confirm your saves survive it (in a threaded build, only with
-DCNA_EMSCRIPTEN_USE_WASMFS=OFF), and a run that reachesExit()if your game exits - Android APK signed with release keystore
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-045: THIRD_PARTY_NOTICES.md still omits the vendored cgltf and stb image headers (and ENet and dr_libs) — Draco and stb_vorbis now have notices, but cgltf, stb_image/stb_image_write, ENet and dr_flac/dr_mp3, all vendored under third_party/ and compiled into CNA libraries, are not listed.