Packaging a .clmod
Status: Implemented. Package signatures (
clight sign), asset and fallback checks inclight check, and checking WASM imports against the API are Planned.
A CLight mod is distributed as one file: <mod_id>-<version>.clmod. This page describes the package format, how clight build and clight pack produce it, and what the loader checks when it opens one.
The format
A .clmod is a zip archive (deflate or stored entries) with a fixed layout:
ruby_tools-1.2.0.clmod
├── clight.mod.json required: the manifest (always at the root)
├── icon.png optional: 64–256 px square
├── bin/ code, depending on runtime.type
│ └── ruby_tools.wasm wasm (C, C++): one module for every platform
├── scripts/ lua: main.lua and modules
├── assets/<ns>/… resource-pack layout (textures, models, lang, sounds.json + sounds/, ui)
├── datapack/<ns>/… data-pack layout without the data/ level (recipes, loot, tags, enchantments)
├── packs/<name>/ optional built-in packs listed in the manifest's "packs"
├── api/*.clidl reserved for service interfaces (planned)
├── LICENSE recommended
└── META-INF/ reserved for signatures (planned)
Rules:
- Paths use forward slashes and are case-sensitive, even on Windows.
clight checkrejects names that differ only by case. - No absolute paths,
..segments, backslashes or symbolic links.clight check, the loader and the website refuse archives that contain them. - Only these top-level entries are allowed:
clight.mod.json,icon.png,bin/,scripts/,assets/,datapack/,packs/,api/,META-INF/,LICENSE*,README*,CHANGELOG*. Anything else is a warning inclight checkand is ignored by the loader. data/is not part of a package: it is the mod’s runtime read/write folder (mods/<id>/data/), created when the mod first writes.assets/follows the vanilla resource-pack layout exactly, anddatapack/<ns>/the data-pack layout underdata/. The mod’s own namespace is its mod id; other namespaces (minecraft,c) are allowed for overrides and shared tags.
Building a package
| Command | Does |
|---|---|
clight build |
Compiles (wasi-sdk for C/C++), validates, and writes build/<id>-<version>.clmod |
clight build --release |
Optimised and stripped WebAssembly |
clight pack |
Packs without compiling (Lua and data mods, or a prebuilt build/bin/) |
clight check |
Validates an existing project or .clmod |
clight sign |
planned: signs the package (Signing and trust) |
CMake workflows use the <mod>_clmod target that clight_add_mod() creates.
Reproducible packages
clight pack writes entries in sorted order with fixed timestamps (1980-01-01), fixed attributes and a fixed compression level (deflate 9, stored when that doesn’t shrink a file). The same inputs therefore produce a byte-identical .clmod, which makes signatures and hashes meaningful and lets users verify a build.
What clight check validates
| Check | Today | Example finding |
|---|---|---|
| Manifest schema | ✔ | sync missing; id doesn’t match ^[a-z][a-z0-9_]{1,63}$; runtime.type: "native" (not supported: every mod is sandboxed); the old "hooks" key |
| Mixins | ✔ | unknown targets or sites in "mixins" and in a built module’s CL_MIXIN records; side mismatches; overwrite warnings |
| Capabilities | ✔ | unknown capability; fs.read:saves, net.socket, process.spawn (never available) |
| Package paths | ✔ | absolute paths, .., backslashes, names that differ only by case, entries outside the allowed top level |
| Runtime files present | ✔ | runtime.entry points to a missing file; icon missing |
| WASM imports | planned | The module imports a function the declared API doesn’t have |
| Assets | planned | Missing lang keys, models referencing missing textures, unused files |
| Fallbacks | planned | Static entries without vanilla_fallback in a mod with vanilla_clients: allow |
| Size | planned | Uncompressed textures over 1 MiB, debug info in a release package |
clight check --strict turns warnings into errors (use it in CI).
What the loader does with a package
- Discover: reads
clight.mod.jsonfrom each.clmodand mod folder inmods/. - Unpack: a
.clmodis unpacked once intomods/<id>/with a.clmod-sourcestamp, so packages and folders behave the same; an updated package replaces the folder (itsdata/is kept). - Resolve: versions, dependencies,
breaksand load order (Versioning and dependencies). - Mount:
assets/becomes the resource packmod/<id>(above vanilla, below the player’s resource packs, so packs can retexture mod content);datapack/becomes the data packmod/<id>(above vanilla, below the world’s datapacks). Mods mount in load order. - Load code:
- Lua: scripts are read from the mod folder inside its sandbox.
- WASM: compiled with Cranelift on first load and cached under
cache/clight/.
Size guidelines
- Compress textures as PNG; avoid huge textures (the atlas has limits and VRAM matters on integrated GPUs).
- Sounds as Ogg Vorbis, mono for positional sounds (stereo sounds don’t attenuate with distance, as in vanilla).
- Build releases with
clight build --release(debug info stripped); keep a dev build if you want symbolized traps.
What the Celestial website checks
Uploading a version on the website (Distribution) runs the same rules again, without unpacking anything to disk:
| Check | Result |
|---|---|
| Unsafe paths, links, files that are also folders, names that differ only by case | refused |
Native code (.exe, .dll, .so, .dylib, executables found by their magic bytes) |
refused: mods are sandboxed |
.wasm files without the WebAssembly header, or another binary version |
refused |
| The manifest: valid JSON, at most 256 KB, the same id as the project, a new version number | refused otherwise |
icon.png: a PNG up to 256 KB (64-256 px square recommended) |
refused / warned |
No LICENSE file, no icon, data/ in the package, nested archives, textures over 1 MB, unknown top-level entries |
warnings |
Then a background scan looks for things that have no reason to be in a sandboxed mod (imports outside the CLight API, Lua bytecode, obfuscation, packed payloads) and marks the file clean or flags it for a moderator.