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 |
|---|---|---|
| No save persistence on the web | Emscripten builds only | SDL_GetPrefPath resolves to volatile MEMFS; CNA mounts no IDBFS and never calls FS.syncfs, so every save is silently discarded on reload. Implement persistence yourself (localStorage / IndexedDB via EM_JS) or ship the web build as a demo without saves. See Tutorial 81. |
| No video | Windows, Web and Android | The video translation units are excluded from those builds. Headers still exist, so code using Video/VideoPlayer compiles and then fails to link. Guard it out per platform, or drop cutscenes from those targets. |
| FFmpeg is a hard requirement | Linux and macOS builds | No option disables it — configure fails without the dev packages. Your shipped Linux binary therefore links FFmpeg runtime libraries, so bundle them (AppImage handles this) and account for their licences. |
Game must be heap-allocated on the web |
Emscripten builds only | A stack-allocated Game is silently corrupted and fails frames later as an indirect-call fault. Allocate with new/std::make_unique. |
Two more worth knowing before you commit to a target. iOS support is narrow and experimental: alpha.1 CI final-links an SDL_RENDERER device app and launches 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 on explicitly enabled SDL_GPU, EasyGL-family and Vulkan builds. 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, whose source declares a CNACApi install component and
CNA::CApi/CNA::CApiStatic targets. Alpha.1 cannot compile that final C implementation because its renderer identity map omits NanoVG, 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 50 current
identities — old names such as EASYGL, D3D9, D3D11, D3D12,
DX3 and ASCII fail configuration. See
Tutorial 72.
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. OPENGLES3 is NOT valid here — it is gated to non-Emscripten
# targets. The Emscripten renderers are WEBGL2 (default), WEBGL1, CANVAS,
# HTML_DOM and SVG_DOM.
cmake -S . -B build-wasm \
-DCMAKE_TOOLCHAIN_FILE=$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake \
-DCNA_GRAPHICS_RENDERER=WEBGL2 -DCMAKE_BUILD_TYPE=Release
cmake --build build-wasm
# CNA ships a "web" preset that configures exactly this:
# 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
- If using Box2D: MIT license
- If using Bullet: zlib license
All permissive — no copyleft restrictions. 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. Alpha.1 contains 21 workflow files spanning intended general tests, focused subsystems, Emscripten, Apple/Metal, platform abstraction, runtime multi-renderer selection and five declared C API gates. Its intended unfiltered general job and two Input rows fail configuration because they still select the removed EASYGL identity, while the C API final target separately fails its renderer-count assertion. Windows D3D/GDI lanes are manual, and the remaining matrix is not exhaustive across 50 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 — alpha.1 contains 568 C++ test sources and 8,263 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). Note
.m4a/.aacare unplayable — SDL3_mixer has no 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 you are not relying on saves that do not persist
- Android APK signed with release keystore