Skip to content
SHINR DOCS
GITHUB ↗

Creating a Module

This guide walks through writing a module and sharing it, using the Shinro CLI (invoked as cake). It assumes a project already initialized with cake init, which creates the project’s .cake/app.toml. It covers the draft_modules/ folder and how to use it, adding a remote registry so modules can be shared with other projects, writing a module descriptor from scratch, how stores work and how to reuse one across projects, and a few rules worth establishing early: avoid long inline scripts, write small tools instead, and never hardcode local paths in a descriptor.

Two terms carry precise meanings on this page. A registry is a descriptor index: a git repository of module descriptors, resolved by name. A store is where a module’s acquired or built content actually lives on disk. Registries answer where the recipe comes from; stores answer where the result lands.

The draft_modules folder

cake init creates a draft_modules/ directory at the project root and wires it into app.toml as:

[registry]
local = "draft_modules"

This is a local registry. Anything placed at draft_modules/<name>/<version>.toml is immediately usable by cake build, the same way a module from a remote registry would be. There is no separate “publish” step required before a module declared here can be built.

The folder name itself is hardcoded by cake init (not configurable): every cake project gets a draft_modules/ folder with this exact name.

Using it

  1. Write a module descriptor at draft_modules/<name>/<version>.toml (see Writing a new module for the descriptor format). If what you actually want is a product belonging to the project itself rather than a standalone dependency, use cake generate <product_name> [--module <module_name>] instead: it creates or reuses a module (the workspace module named after [app].name by default, or <module_name> if given) and adds the product to that, rather than a module called <product_name>.

  2. Reference a hand-written module from app.toml, either unqualified or with the local. prefix; both resolve to draft_modules/:

    [modules.mything]
    module = "mything/1.0.0" # unqualified - searches local first, then remotes
    # or, equivalently and unambiguously:
    module = "local.mything/1.0.0"
  3. Iterate with cake check (fast, validates wiring without building). For a cake generated product, cake build <product_name> builds it directly; a hand-written module builds as part of graph resolution once something requires it (see Writing a new module).

Promoting a module to a shared registry

Once a module in draft_modules/ is ready to share, copy it into a remote registry with:

Terminal window
cake promote <name> <registry> [--version <ver>]

This:

  • Reads draft_modules/<name>/<version>.toml (auto-detects the version if there is exactly one; otherwise pass --version).
  • Copies it into the target registry’s local clone (build/.registry/<registry>/<name>/<version>.toml).
  • Rewrites every module = "..." reference to that module in app.toml (both [modules.<alias>] block format and legacy flat [modules] alias = { ... } format) so it points at <registry> instead of local.

It deliberately does not run git add/commit/push. The registry clone is left with an uncommitted change for whoever owns that registry to review and push. The target registry must be a clone-mode remote (see clone vs fetch mode); fetch-mode remotes have no local clone to copy into.

Adding remote registries

A remote registry is a git repository shaped like a flat folder tree: <name>/<version>.toml per module, same as draft_modules/.

To register one:

Terminal window
cake add <name> <url>

This adds an entry to [registry.remotes] in app.toml and clones the registry locally so modules can be resolved from it immediately:

[registry.remotes]
official = "https://github.com/Shinro-xyz/gh-catalog"
mine = { url = "[email protected]:you/your-registry.git", mode = "clone" }

Other registry commands:

Terminal window
cake update <name> # re-clone a registry to pick up its latest content
cake list modules <name> # list modules (and versions) available in a registry's local clone

clone vs fetch mode

  • clone (what cake add uses): the full registry repo is cloned to build/.registry/<name>. Required if you ever want to cake promote into this registry, since promote needs a writable local clone.
  • fetch: downloads individual descriptor files over raw HTTP without a full clone. Lighter weight, but read-only; you cannot promote into a fetch-mode registry.

Once a remote is registered, reference its modules the same way as local ones, qualifying with the registry’s alias:

[modules.something]
module = "official.something/2.1.0"

draft_modules/ (local) is for modules that only this project needs, or that are not proven enough to share yet. A remote registry is for anything another project should be able to depend on; promote to one once a module has stabilized, instead of copy-pasting the descriptor between projects.

Writing a new module

A module descriptor is a single .toml file. Wherever it lives (draft_modules/<name>/<version>.toml, or a registry clone at <registry>/<name>/<version>.toml), the shape is identical. A full section-by-section descriptor reference (products, steps, actions, resolvers, conditional blocks) is not yet published; this guide covers only the basics needed to get a first module working.

Minimal example

This mirrors cake’s own module descriptor, which describes the CLI itself as a purely local, no-acquisition module:

[module]
name = "cake"
version = "0.1.0"
type = "tool"
store = "local"
[acquire]
strategy = "data"
path = "."

Or skip writing this by hand: cake generate <product_name> [--module <module_name>] creates the module (and a stub product) automatically the first time it is called for that module name.

Module fields

The [module] section declares the module’s identity:

FieldRequiredNotes
nameyesMust match the descriptor’s filename path: <name>/<version>.toml.
versionyesMust match the filename.
typeno"library" (default) | "toolchain" | "tool" | "sdk" | "data" | … (deprecated)
storenoWhere built artifacts are cached, see Stores.

The type field is slated for deprecation and may be removed in a future version.

Acquire: how cake gets the module’s content

strategy selects how cake obtains the module before building it:

StrategyUse forKey fieldsStatus
"data"Purely local, no download, files already live next to the descriptor (or at a relative path). What cake itself uses.pathCurrent
"git"Clone a git repository at a ref/commit.url, refDeprecated
"github-release"Download a versioned release archive from GitHub.repo, tag, artifactsDeprecated
"steps"Custom multi-step acquisition (download + extract + run commands).steps, update_stepsCurrent

For a project-local module (the common case for things living in draft_modules/), strategy = "data" with path = "." is usually enough. The module’s content is whatever sits alongside its own files.

Depending on other modules

A descriptor can declare its own sub-module dependencies with [modules.<alias>] blocks, the same syntax used in app.toml:

[modules.zig-mode]
module = "gh.zig-mode/1.0"
requires.params.mode = "release-fast"
[modules.zig-layout]
module = "gh.zig-layout/1.0"
requires.params.root = "cli"
requires.params.name = "cake"
requires.params.layout = "src-app"
requires.params.artifact_layout = "host-bin"
requires.params.mode = "release-fast"
[modules.zig-build]
module = "gh.zig-build/1.0"
requires.mode_options = "zig-mode.build_options"
requires.layout_options = "zig-layout.build_options"
requires.params.build_file = "{zig-layout.build_zig}"
  • module = "<alias>.<name>/<version>": the alias (gh here) must match a registry already configured in [registry.remotes] (or be local/absent to resolve from draft_modules/).
  • requires.<key> = "<alias>.<provides_key>" wires another sub-module’s output into this one.
  • requires.params.<key> = "<value>" passes a literal parameter.

Provides: what this module exposes

Declare what a consumer of this module can reference, typically by pulling values out of its own sub-modules:

[provides]
binary = "{zig-layout.binary}"
ok = "{zig-build.ok}"

A project consuming this module then references {alias.binary}, {alias.ok}, and so on, the same way this descriptor references its own sub-modules.

Stores

A store is where a module’s acquired or built content actually lives on disk. Separating where you get the wiring and recipe (registries, above) from where the result lands (stores, here) is what lets two unrelated projects share a download or a build without sharing source.

The built-in stores

store valueMeaning
"system"Default store, shared machine-wide: every cake project on the same machine resolves it to the same path unless overridden.
"local"Always rebuilds into this project’s build/, never cached, never shared.
anything elseA custom name, resolved against [stores] in app.toml or a store registered via cake store add (below). Errors if undeclared.

A descriptor only ever writes store = "system" or store = "local"; see Rules worth keeping for why custom or local-machine paths do not belong in a descriptor at all.

Registering a named store

Terminal window
cake store add <name> <path> --portable
# writes to .cake/bin/config.toml : every project within *this* cake's project sees it

For a store scoped to just this one project (no other project should ever see it), skip cake store add entirely and declare it directly in .cake/app.toml:

[stores]
build_cache = "build/.cache"

This is the per-project equivalent of --global/--portable: it lives in the project’s own manifest, so it is only visible to whoever builds this project, and it travels with the repo instead of needing a one-time cake store add on every machine.

Once registered, point a module instance at it the normal way:

[modules.sdl3]
module = "official.sdl3/3.2.4"
store = "shared_deps"

Reusing a store between projects

Two different cake projects share an acquired module automatically whenever they resolve the same store name to the same absolute path and request the same module name + version + params. Concretely:

  • The default "system" store already does this for free: every cake project on the same machine shares it unless you override it, so a common dependency (a compiler toolchain, a library at a given version) is only ever downloaded or built once.
  • For an explicit shared cache (a team-wide dependency cache on a network drive, or a dedicated path you want to keep separate), run cake store add shared_deps /path/to/cache --global once per machine: every project that declares store = "shared_deps" afterward resolves to that same path without needing its own [stores] entry.
  • --portable only helps reuse across projects that invoke the same copy of a cake project folder created by cake init.

Rules worth keeping

Avoid long inline scripts

It is easy to end up with a [steps.build] whose ops is one multi-hundred-character PowerShell (or shell) one-liner: the cpp-clang and cpp-cl modules are real, working examples of exactly this, and they are hard to read, hard to test in isolation, and hard to diff when something changes. Prefer, in order:

  1. A short step calling a small dedicated tool (see below) that does the real work: diag-tool is the pattern to copy.
  2. A checked-in script file (build.bat/build.sh) invoked directly as the step’s exe, if a tool is overkill and the logic really is just “run this compiler with these flags” with no real per-file looping or ordering. If you do reach for this, avoid cmd.exe /c "<one-liner>". The cmd.exe non-standard /c quoting rules are fragile even with a clean argv array from the module’s side. Point exe straight at the script file instead.
  3. A one-liner directly in ops, only for something genuinely one command long.

Build a tool when there is no existing alternative

When the logic is more than “run this compiler with these flags” (parsing output, generating a file, doing anything stateful), write a small standalone program (Zig, C++, C, Python, whatever fits) and call it from a step, rather than growing the inline script to do it. Three real examples already in the catalog:

  • diag-tool: parses raw compiler stdout into structured diagnostics.json. Every C++ builder (cpp-cl, cpp-clang, cpp-gcc, and so on) pipes its build output through it instead of each reimplementing its own diagnostic scraping.
  • cxx-module-build: the per-file ordering a C++20-modules build needs (precompile each .cppm before anything that imports it, in dependency order, then compile and link) is exactly the kind of stateful logic this rule is about. It is what the cpp-clang-cxxmodules module calls instead of a script, which is why builds that use it need no hand-written script file at all.

All three are themselves ordinary modules (strategy = "steps", building their own source with zig build). Wire them in like any other module, and require their binary provide from the step that needs to run them.

Never hardcode local paths in a descriptor

A descriptor should never contain a real machine path (an absolute path into a user home directory or a drive root) or assume it is running from a specific directory other than its own {store}/path.

The only store values a descriptor itself should ever write are "system" or "local". Any project or machine-specific path (a custom store path, a real toolchain install location) belongs in app.toml, never inside the descriptor file.

This is what makes cake promote safe to copy a descriptor verbatim into a shared registry, and what makes the same descriptor work unmodified on a teammate’s machine or in CI: nothing inside it is specific to where it was authored.

Putting it together

By default, cake generate/cake build operate on a single workspace module, the one named after [app].name. Pass --module <name> to either command to target a different module instead. That target module can come from any of three places: create a new one, reuse one already pinned in app.toml, or write its descriptor by hand, and which one you pick is a per-product choice.

The project’s own main product. The common case, no --module needed:

  1. cake generate hello: scaffolds the workspace module (if it does not exist yet) and a stub product called hello inside it.
  2. Edit the generated [steps.hello.build] to do the real build (see Rules worth keeping for how to keep that step small).
  3. cake check to validate the wiring fast, then cake build hello to actually build it.

A separate, unrelated product. Anything that should not share a descriptor with the main product: a flashing pipeline, a shader compiler, a second app. Use the same commands, with --module to give it its own name/version/descriptor instead of piling into the workspace module:

Terminal window
cake generate flash --module flasher_esp32
# -> creates [modules.flasher_esp32] + draft_modules/flasher_esp32/0.1.0.toml
cake build flash --module flasher_esp32

Both --module invocations for the same name reuse whatever version is already pinned in app.toml. Only the first cake generate --module flasher_esp32 creates a version; later calls (for more products in that same module, or to build) reuse it.

A dependency that is not a product of this project at all:

  1. Write draft_modules/mything/1.0.0.toml by hand (start from the minimal example above).
  2. Reference it from app.toml: [modules.mything] with module = "mything/1.0.0".
  3. cake check to validate the wiring, then build it the same way the workspace module’s other dependencies get built as part of graph resolution when something requires it.
  4. When it is ready to share, register a registry once with cake add <name> <url> (skip if already added), then run cake promote mything <name> --version 1.0.0. Commit and push the registry clone yourself; cake promote will not do it for you.

To publish a module to the official Shinro catalog, see The Module Catalog for the full contribution workflow.

Wrapping an existing framework

Wrapping a standalone framework — such as the Python modules architecture control stack — as a library module is a future guide, not covered here yet: today that framework is used directly as Python, with no descriptor. Nothing in this page should be read as describing a cake-wrapped Python modules descriptor that exists now.

100%