Skip to content

Packaging & distribution

A Plume3D app is a plain folder — a game.toml, a main.wren, compiled shaders under shaders/bin/, and assets. To hand it to a player you turn that folder into a single distributable: a .p3d package wrapped in a native shell for the target platform. The plume3d-pack tool does this in one command.

A .p3d is a zip of the cleaned app directory plus a package.toml manifest at its root. The runtime mounts it exactly like a folder — a bundled .p3d sitting next to the runtime is loaded automatically, so a packaged app just runs when launched.

Packaging cleans the app: it drops the plume3d binary/symlinks, *.log, __pycache__/, .DS_Store, and each app’s shaders/compile.sh, and keeps your main.wren, game.toml, shaders/bin/*.spv, and assets. Compiled SPIR-V (shaders/bin/*.spv) must be present — the runtime loads the compiled shaders, not the .slang sources. Compile them before packaging, or pass --compile-shaders to have plume3d-pack run slangc for you (see below); without it the tool only warns when a .slang has no compiled output.

plume3d-pack is a separate host tool, not part of the runtime. Point it at an app folder and a target, and give it the shipping runtime binary to wrap (build one with -DPLUME_VARIANT=shipping — see Command line → Build variants):

plume3d-pack <appDir> --target macos|windows|linux --runtime <plume3d-binary> [options]
OptionMeaning
--target macos|windows|linuxwhich platform shell to produce (required)
--runtime <path>the plume3d binary to bundle — use a shipping build (required)
--variant shipping|developmentrecorded in package.toml; default shipping
--id <reverse-dns>app id, e.g. com.studio.game; default derived from the name
--name <name>display name; default from the app’s game.toml
--entry <script>entry script; default main.wren
--compile-shadersrun slangc on shaders/src/*.slang → shaders/bin/*.spv before packaging
--shader-include <dir>override the auto-detected engine/shaders/include (the shader ABI headers)
--out <dir>output directory; default the current directory

It produces:

  • macOS — <Name>.app (unsigned): Contents/MacOS/plume3d, Contents/Resources/<id>.p3d, and an Info.plist. Code signing and notarization are a separate, credential-gated step.
  • Windows — a folder (plume3d.exe + <id>.p3d) zipped to <Name>-windows.zip.
  • Linux — a folder (plume3d + <id>.p3d) as <Name>-linux.tar.gz.

Example — package the triangle sample as a macOS app:

plume3d-pack apps/plume_triangle --target macos \
--runtime build/shipping/plume3d --out dist/

Pass --compile-shaders and plume3d-pack runs slangc on every shaders/src/*.slang into shaders/bin/*.spv before it bundles, so you can package straight from sources:

plume3d-pack apps/my_game --target linux \
--runtime build/shipping/plume3d --compile-shaders --out dist/

It resolves slangc from $VULKAN_SDK, common Vulkan SDK install paths, then your PATH (install the Vulkan SDK if it isn’t found — the tool never downloads a toolchain). App shaders that #include the engine’s shared shader headers resolve automatically; pass --shader-include <dir> if your engine/shaders/include lives somewhere the auto-detect misses. Without the flag, packaging leaves your existing shaders/bin/*.spv untouched (and warns if a .slang has none).

The manifest is written for you by plume3d-pack; you normally never edit it. It is the frozen schema the runtime reads at mount:

[package]
id = "com.studio.game"
name = "My Game"
version = "1.0.0"
entry = "main.wren" # the script loaded on run, --server, and --check alike
[engine]
variant = "shipping"
version = "1.0.0"
[window]
width = 1280
height = 720
resizable = true
fullscreen = false
borderless = false
scale_mode = "letterbox"

package.toml is authoritative for the app’s name and version. Its [window] is used only when the bundle ships no game.toml; when a game.toml is present its own [Window] (including design resolution) drives the window. A .p3d is untrusted input, so the runtime validates the manifest on load: an entry that is absolute or contains .. is rejected back to main.wren (it can never point outside the package), the id charset is restricted, and window dimensions are clamped.

A .p3d can be signed so the engine verifies it on every launch and refuses to run if the package was modified after packaging — if a player edits your scripts, models, or maps, the game fails to launch. Signing is the paid licensing tier; the engine itself is free, and an unsigned package runs normally (without this verification).

A licensed developer generates an Ed25519 key pair and receives a developer certificate signed by WyldMagic. plume3d-pack then embeds a signed integrity chain in the .p3d:

plume3d-pack <appDir> --target macos --runtime <plume3d> \
--dev-key <your-ed25519-private-key> --dev-cert <developer.cert> ...

This writes four files at the package root — developer.cert (+ its WyldMagic signature) and an integrity.manifest of per-file SHA-256 hashes (+ your signature). At launch the runtime verifies the chain against the WyldMagic root key baked into the engine, then checks every file’s hash, and refuses to load on any mismatch.

This is tamper-evidence, not encryption — it detects modification of a shipped package; it does not prevent someone extracting assets from it (no client-side scheme can — see the licensing docs). It pairs with OS code signing (which the OS enforces at first launch); the in-engine check runs at every launch.

OS code signing / notarization (Apple Developer ID + notarize, Windows Authenticode) is the credential-gated follow-up that removes Gatekeeper/SmartScreen warnings — separate from the in-engine integrity above. Native installers (Windows NSIS/WiX, Linux AppImage) and mobile/web shells are also follow-ups. (The earlier macOS OpenSSL-dylib gap is resolved — the shipping runtime static-links OpenSSL, so a packaged .app is self-contained on a clean Mac.)

See also: Command line, Configuration, Get Plume3D.