Skip to content

Native game release packages

The installed ludus host tools can build and validate a native Linux x64 or macOS arm64/x64 Release package for a version-2 CMake game project. Packaging is independent of an itch.io account. This implements the first part of the packaging and publishing architecture. Editor setup, browser packaging and automated uploads are described in Editor-managed game releases.

Native ZIP manifests (4 MiB), central directories (8 MiB), Mach-O load-command tables (8 MiB) and native inspection output (8 MiB) use named policies from scripts/python/ludus_tools/package_limits.py. These encoded metadata and diagnostic ceilings are independent of the engine's scene and Content limits.

Create a project with release files

Use an installed Release SDK that matches the project's engine requirement and the consuming toolchain. Other SDK flavors remain valid for development but cannot produce a Release package. The example below selects macOS explicitly; use linux-clang-release for Linux.

ludus project create MyGame --name MyGame --sdk /path/to/release-sdk --profile macos-clang-release --tools /path/to/Ludus --release

Add --itch-target username/game to generate an optional destination mapping for offline upload planning. Nothing authenticates or uploads during creation. Without --release, creation retains its ordinary project layout. The minimal native template is version 4; existing projects are never overwritten.

Generated ludus.release.json selects a release profile, executable target, install component and entry point. cmake/GameRelease.cmake installs the executable, NOTICE.txt and SDK license files. Edit its explicit install rules to add runtime assets, dependencies and the corresponding notices. The generated notice is a starting point: the game author must complete the license inventory. CMake remains authoritative for the payload; no build-tree directory sweep occurs.

Existing projects can use ludus project release init or the Editor release setup action; see the setup guide. The release workflow shares its toolchain steps, tools pin and SDK variables with the build and test workflow.

Build and inspect the package

Required host tools are Python 3.10+, the project's pinned CMake/Ninja/compiler and GNU readelf on Linux, or Apple's /usr/bin/codesign on macOS. Atomic publication uses Linux renameat2 or Darwin renamex_np(RENAME_EXCL). SDK identity and Release flavor must match the selected platform and compiler. Linux uses linux-release / linux-x64; macOS uses macos-release and macos-arm64 or macos-x64. macOS package verification requires a macOS host.

ludus project package MyGame --profile linux-release --version 0.1.0
ludus project package MyGame --profile linux-release --version 0.1.0 --sdk /path/to/release-sdk
ludus project package verify /path/to/package-directory

The command holds the shared Editor/CLI build-tree lock throughout configure, build and installation. It requires a successful current build, checks the CMake File API artifact's program sections against the installed entry point while allowing CMake's loader-path rewrite, and validates SDK stamps and project descriptor/lock/release configuration. Temporary staging is private and cleaned on failure. Packages publish as complete directories through an atomic no-replace rename. Identical verified packages are reused.

Output is MyGame/out/packages/<profile>/<archive-sha256>/, containing game.zip, package.json and validation.json. The SHA256 identifies the exact archive. ZIP entries are sorted with fixed timestamps and normalized permissions. This is deterministic archiving of identical payloads, not a claim of reproducible compilation. Keep out/ ignored. Do not mutate a completed package: verify and plan recheck its content rather than treating its pathname as proof of validity.

Extraction validation checks entry count, path lengths, expanded sizes, types, case collisions, permissions, payload hashes, entry point, notices and native runtime dependencies. It never executes the binary or invokes ldd. Regular ELF libraries must be explicitly installed under loader search paths using $ORIGIN; symlinks are rejected. Install libraries under the names actually required by DT_NEEDED, including a regular copy for a versioned SONAME.

The initial external runtime policy allows only the Linux C/C++ ABI libraries: libc, libm, libdl, libpthread, librt, libstdc++, libgcc_s, libc++, libc++abi and libunwind, with specific SONAMEs recorded in package_native.py. Unknown native dependencies fail until bundled or supported by a reviewed policy revision. The manifest records the libraries actually required and the SDK's distro and system prerequisites. SDK headers/static archives, debug sidecars, debug sections and observed producer paths are rejected. License presence checks do not certify that the author supplied every legally required notice.

macOS app packages

Create against a matching Release SDK with --release, or explicitly set up an existing project. Setup defaults to macOS on Darwin; --platform macos selects the current host architecture. The existing setup is never overwritten.

ludus project release init MyGame --platform macos
ludus project repair MyGame --tools /path/to/Ludus --profile macos-clang-release --sdk /path/to/release-sdk
ludus project package MyGame --profile macos-release --version 0.1.0 --sdk /path/to/release-sdk
ludus project package verify /path/to/package-directory

The generated GameRelease component installs <target>.app, NOTICE.txt and SDK licenses. The entry point is <target>.app/Contents/MacOS/<target> and must agree with Info.plist. Install authored assets under Contents/Resources and regular runtime dylibs under Contents/Frameworks, using the same component. CMake sets INSTALL_RPATH to @executable_path/../Frameworks. Runtime dylibs need relative install names and their own relative search paths. The validator accepts @loader_path / @executable_path paths within the payload and resolves @rpath against each image's declared paths; inherited dyld run-path stacks are outside this initial policy. Absolute producer paths and unresolved dependencies fail.

Policy macos-release-adhoc-1 requires thin little-endian 64-bit Mach-O images with matching CPU architecture and a deployment baseline no newer than macOS 14.0. Bounded command, section, symbol and entry tables are inspected without running the game. Debug sections/symbols, foreign/fat binaries, symlinks, escaped loader paths and loader environment commands are rejected. The closed external baseline consists of Apple's system C/C++/Objective-C dylibs and the named system frameworks in package_macos.py; every actual dependency appears in the manifest. Games linking Ludus::Platform and Ludus::GraphicsRhi (including through Ludus::GraphicsRenderer) load libbsm, IOSurface and MetalFX. MetalFX first shipped in macOS 13.0, so an image with an older baseline must weak-link it. Unknown dependencies must be explicitly bundled or gain a reviewed policy revision.

Packaging signs nested Mach-O images first, then the app, using a local ad-hoc identity and no timestamp. It uses no keychain identity or account credential. The installed program's code/allocated-section identity must match the current CMake artifact despite loader/signature metadata changes. Code and app resource signatures are verified with codesign --verify --strict, again after checked archive extraction. Signature failure, a failed current build, changed inputs or Stop/cancellation prevents package publication.

These signatures enable local validation and execution. Developer ID signing, notarization, Gatekeeper qualification, universal binaries, symlinked framework deployment, DMG installers and a distributable Qt Editor are separate work. Native Apple silicon relocation/execution is covered by acceptance; x64 identity and malformed-image tests do not establish native Intel runtime acceptance.

Supplying --tools-ref <immutable-Ludus-commit> also generates a native GitHub workflow (macos-15, arm64 only) for tags and manual dispatch. An x64 workflow is refused: hosted Intel runners have no prebuilt LLVM 23. Set LUDUS_RELEASE_SDK_URL and LUDUS_RELEASE_SDK_SHA256 for a matching Release SDK archive. Its package job installs Clang 23 and managed CMake/Ninja, verifies the SDK digest and publishes the checked package as a GitHub artifact. It has no upload job or account secret. The native Editor shares this backend through Release > Set Up Releases and Package Release; signing commands retain owned-process cancellation.

Optional itch.io destination macos / channel macos-stable supports offline planning on macOS. The current live upload transport remains Linux-only and fails explicitly on macOS before installing butler or acquiring credentials.

Thanks to Apple for the Mach-O layouts in XNU loader.h, Run-Path Dependent Libraries, and Creating distribution-signed code for macOS. The implementation adopts bounded static inspection and nested-code-first signing; it deliberately uses an ad-hoc identity rather than distribution signing.

Inspect an itch.io plan

ludus project publish plan MyGame --package /path/to/package-directory --destination linux
ludus project publish plan MyGame --package /path/to/package-directory --destination linux --allow-local-inputs

Planning validates the archive, manifest, profile, current release configuration and engine lock. It needs no installed SDK, butler, account login or network. It reports destination/channel, version, exact digest, payload bytes and a future butler argument plan with a placeholder for a verified private snapshot. The reported plan is not an executable upload command. Explicit uploading is available through project publish upload, described in the setup guide.

Packages from dirty/unknown sources or local SDK overrides require the explicit local-input option. This is a planning choice; no upload occurs. A channel named beta or staging does not change the itch.io project's visibility. BUTLER_API_KEY is stripped from project commands and compiler/native inspection subprocesses. Do not store credentials in the project or its manifests.

Global --json emits schema-1 CLI results. Configuration and package failures use stable InvalidRelease, UnsupportedReleaseTarget, InvalidPackage, Conflict, BuildFailed, SdkIncompatible and MissingTools categories through the existing host-tooling error boundary. A failed operation never uploads.

Install the pinned butler tool

The installed CLI can explicitly acquire butler 15.31.0 for Linux x64, macOS Apple silicon, or macOS Intel:

ludus tools install-butler /path/to/private-tools
/path/to/private-tools/butler version
# Machine-readable installation result:
ludus --json tools install-butler /path/to/another-tools-directory

The running host's OS and CPU select the versioned upstream archive. A Python process running under Rosetta reports x64 and selects the Intel executable; native arm64 Python selects the Apple silicon executable. Other hosts fail before downloading or creating directories.

Both archive and executable SHA-256 values are pinned in the shared installer. Downloads and extracted executable bytes are bounded to 128 MiB; redirects must remain HTTPS. Only butler is installed, with owner-only executable permissions. Optional 7-zip libraries are excluded, following the upstream installation manual.

The destination may be a new or existing ordinary directory. Existing files are preserved; an existing butler, including a symlink, causes Conflict before any download. Use another destination to reinstall. Verified bytes are published atomically without replacement. Failure or interruption removes private staging and a newly created empty destination; parent directories may remain.

Installation does not execute butler, modify PATH/shell settings, use BUTLER_API_KEY, log in, or upload. No project check/open/build implicitly installs it. macOS live upload supervision remains guarded; this command supplies an independently usable pinned tool without enabling project publish upload.

Validation scope

Run python3 -m unittest discover -s scripts/python -p 'test_ludus*.py' -v with the pinned tools on PATH. Release tests compile real native executables, exercise CMake installation, corruption/failure cases and clean-extraction execution. Linux fixture tests use a small SDK. macOS additionally runs test_macos_release and tests/sdk_consumer/verify_macos_release.py with the installed wheel and a relocated full production Release SDK. The Cocoa Editor's [.macos-release-journey] checks real setup, verified packaging and Stop cleanup. See CI for macOS 14 native and sanitizer gates. These checks establish the recorded package/relocation scope; real hosted gameplay needs its own acceptance. The transport is tested with isolated stubs; no live-account upload is part of local validation. See the evidence ledger.