Skip to content

Independent Ludus game projects and shared SDKs — user guide

This guide covers installing the host tools, installing and overriding/refreshing SDKs, project creation and migration, building with the CLI (and the Editor), direct CMake use, recovery and the current compatibility limits. It documents the workflow implemented by .kiro/specs/project-sdk-workflow.

On macOS, New Project and Repair Project Setup reuse the Apple SDK prepared in that engine checkout whenever they prepare the editor's engine, including automatic preparation of a missing or stale engine installation. An unrelated SDKROOT inherited by the GUI does not replace it. If no prepared SDK exists, init tests installed SDKs automatically. Running ./init.sh yourself continues to honor an explicit shell SDKROOT override.

Scope. Native project tooling supports Linux x64 (Ubuntu 24.04) and macOS with pinned upstream Clang 23, single-config Ninja, and Debug/Development/Release SDK identities. The installed macOS CLI has native Apple silicon acceptance; Intel identity/architecture selection is covered by unit tests and still needs native Intel acceptance. Windows MSVC project setup covers the installed Foundation, Input, Platform, and RHI SDK. General scene authoring requires separate acceptance. Browser releases and native live editing have their own implemented scope; an embedded viewport remains future work. Do not assume unsupported combinations work.

Prerequisites

Host tools (the ludus CLI): Python ≥ 3.10. No Qt, no engine checkout.

Building a generated game additionally needs the documented toolchain for a Ludus SDK: CMake 3.29.6, Ninja 1.11.1, and upstream Clang 23. Linux uses LLD 23; macOS uses the prepared Apple SDK and Clang 23 libc++ headers with the deployment baseline recorded in the SDK manifest (currently macOS 14.0). For shaders, use the pinned Slang compiler (config/shader_toolchain.json): macOS emits Metal source; Linux and browser SPIR-V paths also require spirv-val. A windowed game also needs its runtime display/GPU environment (Metal on macOS, a Vulkan loader and driver on Linux); headless tooling does not imply a windowed game runs without a compositor/GPU.

Install the host tools

python3 -m venv out/ludus-cli-venv
out/ludus-cli-venv/bin/python -m pip install ./scripts/python
out/ludus-cli-venv/bin/ludus --help

Activate that venv, or use its bin/ludus path explicitly for the commands below. The installed ludus launcher locates its own packaged modules; it does not need a Ludus source checkout.

Install an SDK

# From a local archive (checksum optional but recommended):
ludus sdk install --archive ludus-sdk-development.tar.gz --digest <sha256>

# From a publisher release catalog:
ludus sdk install --version 0.1.0 --target linux-x64 --flavor development \
    --catalog https-or-local-catalog.json

ludus sdk list

SDKs install into a shared user store (Linux default ~/.local/share/ludus; macOS default ~/Library/Application Support/Ludus). --store, LUDUS_SDK_STORE, and XDG_DATA_HOME retain explicit location precedence. Each SDK directory is immutable and content-addressed, so several projects share one copy. Installation validates archive bounds/containment, the payload digest and the manifest identity, stages to a sibling directory and atomically publishes — a concurrent or cancelled install never exposes a partial SDK, and an existing good SDK is never replaced by a partial one. Repair a corrupt store entry explicitly with --repair.

Create a project

# Against a release (writes an exact engine requirement; the lock stays
# "unresolved" until a published release exists):
ludus project create ./MyGame --name "My Game" --template minimal --engine 0.1.0 \
    --components FoundationBase,GraphicsRhi

# Against a local SDK you built yourself (engine-developer flow). The local
# prefix is recorded ONLY in ignored .ludus/local.json; the committed lock stays
# release-oriented:
ludus project create ./MyGame --name "My Game" --template minimal --sdk /path/to/sdk-prefix

Creation is atomic and no-replace: it refuses an existing/symlink/nonempty destination, stages the complete project, and publishes only to an absent destination (a destination appearing mid-create is never overwritten). Generated files are yours after creation; later template versions never overwrite your game code. The generated CMakeLists.txt/CMakePresets.json are ordinary, editable CMake — project metadata is not a second build language.

macOS creation and setup

Prepare the trusted tooling checkout and install a complete SDK first:

./scripts/init macos-clang-development --preset-only --cli --no-system-install
./scripts/shader-probe bootstrap
./scripts/install-sdk macos-clang-development
out/host-tools/venv/bin/python -m pip install ./scripts/python

Create and verify a project against that SDK:

out/host-tools/venv/bin/ludus project create /path/to/MyGame --name "My Game" \
    --sdk "$PWD/out/install/macos-clang-development" --tools "$PWD"
out/host-tools/venv/bin/ludus project run /path/to/MyGame

--tools explicitly authorizes preparation/verification using that trusted checkout. Without --sdk, creation selects the host's Development profile, reuses only a current matching SDK, or explicitly prepares a separate ABI-keyed install. Preparation bundles the dependency closure before staged game validation. Bundled link interfaces select Apple system frameworks by name from the consumer's SDK, preserving relocation without embedding the producer sysroot. With --sdk, the project profile follows that SDK's native flavor; when combining --tools with another flavor, also pass its full --profile name.

Minimal template version 4 includes macOS configure/build/test presets for macos-clang-debug, macos-clang-development, and macos-clang-release. macOS presets are selectable on Darwin. Each flavor requires its matching SDK; a Development SDK is never used silently for Debug or Release. Repair verifies the selected native flavor with real configure/build/test commands and writes its selectable ludus-local-<profile> presets. Switching flavor requires its matching SDK and explicit repair:

ludus project repair /path/to/MyGame --tools /path/to/Ludus \
    --profile macos-clang-debug --sdk /path/to/debug-sdk
ludus project check /path/to/MyGame --tools /path/to/Ludus --profile macos-clang-debug
ludus project run /path/to/MyGame --profile macos-clang-debug

Each verified native profile retains its own ignored setup record and owned presets. Repairing another flavor preserves those entries and the descriptor keeps its original default profile.

Mac setup uses the matching architecture (arm64 or x86_64), manifest deployment baseline, prepared SDK sysroot, and Clang 23 libc++ headers. Machine paths live in ignored user presets. Retargeted tool/SDK symlinks and stale macOS cache flags require explicit repair. Native flags are excluded from browser presets. The selected compiler's identity is checked before repair writes local settings and before ordinary CLI operations; Apple Clang is not the reference compiler.

Existing version-2 projects keep their source, descriptors, and tracked presets. To port a Linux project deliberately, select its macOS descriptor preset and add the matching project-owned CMake preset (or hidden base), then run repair with the macOS SDK. Repair does not rewrite tracked project intent. Legacy version-1 projects still use the original Linux schema until explicit migration.

The native Qt Editor supports macOS project workflows. macOS release packaging creates and verifies local ad-hoc signed game apps. Universal binaries, Developer ID signing/notarization and native Intel acceptance remain separate follow-ups.

Configure, build, run

ludus project configure ./MyGame --profile linux-clang-development
ludus project build     ./MyGame --profile linux-clang-development
ludus project run       ./MyGame --profile linux-clang-development   # ensures a fresh build first

Configure/build/run never download an SDK, refresh the lock or compile engine sources. SDK resolution precedence is: --sdk <prefix> for this command → the saved .ludus/local.json override → the exact locked release in the store. The resolved identity, path and override status are printed. A failed or cancelled build never launches an older binary. The CLI and Editor share a per-build-tree cooperative lock, so a concurrent build of the same tree reports Busy rather than corrupting it. Raw cmake --preset … works too (set LUDUS_SDK_PREFIX yourself) but is outside the cooperative-lock guarantee.

Continuous integration (GitHub Actions)

A project can carry a GitHub Actions workflow that builds and tests every push to main, every pull request and every manual run. Add it while creating the project, or later to an existing one:

# New project (in the Editor: tick "Add a GitHub Actions build and test workflow"):
ludus project create ./MyGame --name "My Game" --sdk /path/to/sdk-prefix \
    --ci github --tools-ref <40-character Ludus commit>

# Existing project; --platform defaults to the project's preset family
# (macOS uses this host's architecture):
ludus project ci init ./MyGame --tools-ref <40-character Ludus commit>

# Existing project, one build job per listed platform:
ludus project ci init ./MyGame --tools-ref <40-character Ludus commit> \
    --platform linux-x64 --platform macos-arm64

Setup writes .github/workflows/ci.yml and config/ludus-tools-revision.txt. These files are yours: edit the triggers, branches and steps as the project grows. Setup never overwrites a file. An existing tools pin naming the same commit, for example one written by ludus project release init, is reused. A pin naming a different commit is a Conflict. The Editor pins the commit of its trusted tooling checkout. The workflow installs the host tools from github.com/The-Ludus-Project/Ludus at that commit, so it must be published there.

The workflow has read-only repository permissions and uses no secrets. On an Ubuntu 24.04 or native-architecture macOS runner, it installs the pinned LLVM, CMake, Ninja and host tools exactly as the release workflow does. It then builds the platform's Release profile (linux-clang-release or macos-clang-release) and runs the project's CTest tests. Building needs a Release SDK archive. Set the repository variables LUDUS_RELEASE_SDK_URL (HTTPS) and LUDUS_RELEASE_SDK_SHA256 (lowercase SHA-256), the same variables the release workflow uses. If both are unset, a short job reports a notice and the build job is skipped. If only one is set, or either is malformed, the build fails.

The SDK archive may be a release asset of the game repository itself, such as https://github.com/<owner>/<game>/releases/download/<tag>/<asset>. The workflow fetches such an asset with gh release download, authenticated by the job's own read-only github.token, so it works when the repository is private. A private repository serves release assets only to authenticated requests, and its download URL redirects to signed storage that rejects a forwarded token. Any other HTTPS URL is downloaded directly without credentials, so it must be publicly readable. Either way, the archive must match the SHA-256 digest and stay within 4 GiB before ludus sdk install accepts it. The release workflow uses the same step.

A workflow for several platforms has one build job per platform, such as build-and-test-macos-arm64. Each platform needs its own matching Release SDK, so each job reads its own variables, suffixed with the platform: LUDUS_RELEASE_SDK_URL_LINUX_X64 and LUDUS_RELEASE_SDK_SHA256_LINUX_X64, and likewise _MACOS_ARM64. A platform whose two variables are both unset is skipped and named in the notice job; the other platforms still build. macOS jobs run on macos-15; the SDKs keep a macOS 14.0 deployment target. Setup refuses macos-x64: Homebrew ships no Intel macOS bottles for the pinned LLVM and LLVM publishes no macOS x64 release binaries, so an Intel runner would build LLVM from source for longer than a job may run.

An SDK accepts only the exact compiler version it was built with, such as Clang 23.1.4. The workflow installs the current pinned-major LLVM from Homebrew or apt.llvm.org, so a new LLVM patch release makes the build fail with SdkIncompatible until the variables name an SDK built with that compiler. Published engine releases do not yet resolve a project lock, so CI cannot select an SDK by itself.

GitHub runs workflows only from the repository root's .github/workflows. Setup therefore refuses a project inside another Git work tree; add a workflow at that repository's root yourself. A new project is never the root of an existing work tree, so the Editor disables the option for such a location. A project outside any repository is accepted; initialize Git there and push it to GitHub. ludus project ci init reports whether origin is on github.com. It reads .git/config and never runs Git or contacts the remote. GitHub Actions is the only supported provider.

Windows MSVC projects

Use the x64 Visual Studio 2022 developer shell initialized with MSVC 14.44 and Windows SDK 10.0.26100.0. Install Python 3.10 or later and prepare the host tools in the Ludus checkout. Run these PowerShell commands from that checkout.

$tools = $PWD.Path
$env:PYTHONPATH = "$tools/scripts/python"
python -m venv out/host-tools/venv
$python = "$tools/out/host-tools/venv/Scripts/python.exe"
$pins = Get-Content config/tool_versions.json | ConvertFrom-Json
& $python -m pip install "cmake==$($pins.windows_msvc_probe.cmake)" "ninja==$($pins.managed.ninja)"

$project = 'C:/Projects/MyGame'
& $python -m ludus_tools project create $project --name MyGame --tools $tools --sdk 'C:/SDKs/Ludus/Debug'
& $python -m ludus_tools project check $project --tools $tools
& $python -m ludus_tools project repair $project --tools $tools --profile windows-msvc-profile --sdk 'C:/SDKs/Ludus/Profile'
& $python -m ludus_tools project check $project --tools $tools --profile windows-msvc-profile

The minimal template provides windows-msvc-debug, windows-msvc-development, windows-msvc-profile, and windows-msvc-release as configure, build, and test presets. Select a matching installed SDK for each flavor. Development and Profile both use CMake RelWithDebInfo and have separate SDK selections and build trees. Create with --tools verifies the new project before publication. Repair configures, builds and runs CTest with the local preset. Use the resulting ludus-local-windows-msvc-* presets in CMake Tools launched from the same developer environment.

For Visual Studio/MSBuild, use the corresponding windows-msvc-visual-studio-debug, windows-msvc-visual-studio-development, windows-msvc-visual-studio-profile, or windows-msvc-visual-studio-release profile. Each profile generates a solution with the configuration accepted by its installed SDK. Development and Profile use RelWithDebInfo in separate solutions, each linked against its own SDK flavor. The CLI builds that configuration and uses the CMake File API to launch its executable. For example:

$profile = 'windows-msvc-visual-studio-profile'
& $python -m ludus_tools project repair $project --tools $tools --profile $profile --sdk 'C:/SDKs/Ludus/Profile'
& $python -m ludus_tools project run $project --profile $profile

Check reports missing or stale setup without writing files. After moving an SDK or replacing the tooling, run repair with the new paths. Repair preserves other flavors, custom presets, and unrelated IDE settings. For Win32 and D3D12 applications, select the installed Platform and GraphicsRhi components. Repair prepares Slang and DXC for projects using ludus_compile_shader. The Windows workflow uses a local SDK prefix and covers the installed Foundation, Input, Platform, and RHI closure. Editor and GameHost integration, browser setup, and release automation have separate acceptance requirements.

Check and repair project setup

In the Editor choose Project → Check Setup. Eligible project opening also performs this read-only check: missing/stale presets, tools and SDK inputs are reported without downloading, configuring, building or rewriting the game. A failed check shows a one-line Setup needs attention notice above all workspace tabs, including World; its tooltip holds the full problem. Choose Repair setup… to review and confirm repair, or keep editing and repair later. Check again reruns only the read-only check. Save changed project settings before either action. The notice stays through retries and failed/cancelled repairs until setup succeeds; closing or switching projects retires the previous project's notice. ✕ dismisses the notice for that problem only: a different problem shows it again, and the status bar line and Project → Repair Project Setup… remain available.

Save settings, then choose Project → Repair Project Setup and keep Automatic (recommended) unless changing the engine deliberately. Repair preserves custom presets and unrelated IDE settings, refreshes stale owned CMake inputs, and verifies selectable configure/build/test presets plus an actual configure/build/native test run. Browser repair requires a matching SDK.

Repair also prepares the rendering tools required by the project's saved render graphs, including native ray tracing on macOS. It selects and verifies these dependencies automatically; no executable paths or separate bootstrap commands are needed. The same dependency rules drive graph builds and setup checks, so Setup ready cannot overlook a missing ray-tracing translator. Missing tools discovered by a graph build bring back the setup notice with Repair setup…. Existing drawable viewport output is retained while setup needs attention. Tool preparation runs through the background setup job and supports cancellation; failure or cancellation before project verification keeps existing project settings. Opening a project and Check Setup remain read-only. Native editor installation prepares its Metal rendering dependencies up front; projects with only raster graphs do not acquire unnecessary dependencies during repair.

The installed CLI exposes the same operations:

ludus project check /path/to/MyGame --tools /path/to/Ludus
ludus project repair /path/to/MyGame --tools /path/to/Ludus --sdk /path/to/sdk

When the project's engine is older than the Editor

The Editor runs from its tooling checkout, but a game builds against the engine install recorded in .ludus/local.json. Each install's share/Ludus/LudusSdkManifest.json records the revision it was built from. Check Setup compares that revision with the checkout's HEAD, even when the rest of the check fails. When they differ, or when the selection is not an install at all (for example a hand-edited path to out/build/<profile>), the project panel names both revisions and shows Use this editor's engine. That button builds and installs this checkout's engine, selects it, keeps saved browser setup and verifies the project, all in one step.

Automatic repair does the same for installs this Editor owns, those below <checkout>/out/install (or a broken selection inside <checkout>/out). An install you chose elsewhere is never replaced silently: Check Setup reports it and Use this editor's engine is the explicit switch. In Choose a different installed engine…, the dialog lists this checkout's matching installs and rejects build folders, folders without a manifest and other build flavors before repair runs.

Use the CLI instead of editing .ludus/local.json by hand:

ludus project engine ./MyGame --status --tools /path/to/Ludus   # current, stale, invalid or unknown
ludus project engine ./MyGame --list   --tools /path/to/Ludus   # this checkout's installs and revisions
ludus project engine ./MyGame --sdk /path/to/Ludus/out/install/<profile>-<id>

--sdk validates the folder (an install, not a build tree, with the project's target, flavor and engine version) before saving it. Existing local.json files keep working; no migration is needed.

When tools or an SDK move, check and repair explicitly. Keep machine paths in ignored CMakeUserPresets.json and .ludus/local.json; the committed engine requirement/release lock describes portable intent. The IDE should use the selected project-managed CMake in preset mode. A hidden base preset alone does not prove a usable setup. See platform compatibility and Editor operations.

Engine override and refresh (engine developers)

# Point a project's build at a locally built SDK, without touching the lock:
ludus project engine ./MyGame --sdk /path/to/local/sdk --profile linux-clang-development

# After rebuilding/reinstalling that local SDK, just rebuild the game — the
# tooling detects the changed SDK inputs (not just the path) and reconfigures/
# relinks. Local engine install and game build must not run at the same time.
ludus project build ./MyGame --profile linux-clang-development

# Return to the locked release without editing committed files:
ludus project engine ./MyGame --clear-override --profile linux-clang-development

The override is visible in build output and never rewrites the committed descriptor/lock. The engine command changes the local SDK selection; it has no --version option. Committed engine requirements and release locks remain explicit project intent. The migration command below handles legacy version-1 project metadata.

Migrate a version-1 project

ludus project migrate ./LegacyGame --engine 0.1.0

Migration is explicit (Open never migrates silently), preserves relative paths and launch settings, requires an engine selection, and commits the new descriptor and lock together. If a migration is interrupted, leftover *.migrating files mark an incomplete commit; rerun migration or discard them to recover.

Direct CMake (no Ludus CLI)

export LUDUS_SDK_PREFIX=/path/to/installed/sdk-prefix
cmake --preset linux-clang-development
cmake --build out/build/linux-clang-development --target my_game

# macOS, after explicit project repair, select its owned toolchain preset:
/path/to/Ludus/out/host-tools/venv/bin/cmake --preset ludus-local-macos-clang-development
/path/to/Ludus/out/host-tools/venv/bin/cmake --build --preset ludus-local-macos-clang-development
/path/to/Ludus/out/host-tools/venv/bin/ctest --preset ludus-local-macos-clang-development

The generated presets read $env{LUDUS_SDK_PREFIX}. find_package(Ludus CONFIG REQUIRED) resolves the bundled dependency metadata relative to the SDK prefix and performs the usual CMake version/variant compatibility checks.

Recovery and failure modes

  • Unresolved lock + no override → build stops with an actionable message (install the release or set a local override); it never falls back to building the engine.
  • Flavor/toolchain/policy mismatch → rejected before configure with expected-vs- actual detail.
  • Corrupt SDK in the store → explicit ludus sdk install --repair.
  • Interrupted migration → discard *.migrating and retry.

Compatibility limits

SDK identity is derived from the real build inputs (triple, compiler id/version, C++ runtime ABI, flavor, variant, policy, features) — never inferred from a version string. An SDK is used only when that identity is compatible with the request. See the architecture for the rationale and implementation evidence for the recorded validation scope.

Ludus-Sandbox conversion (follow-up)

Ludus-Sandbox should become the reference independent application using this interface (remove its mandatory nested engine build; pin an engine release). That repository is not present in this checkout and has not been inspected, so its conversion is a separate change requiring access to that repository and its own evidence. The reference consumer validated here is the standalone project in tests/sdk_consumer plus the generated minimal template; converting Sandbox is tracked as external follow-up and is not claimed complete by this work.