recipe.toml

Every artifact in this registry is published with a recipe: one TOML file describing one coordinate. It is what a client reads to decide whether to download the blob at all.

The shape

A recipe describes exactly one (kind, name, version, target). Publishing clang for two targets means two recipes and two archives — coordinates are immutable one at a time, and so are recipes.

The four keys at the top are the coordinate. They are the same in both kinds, and they must match what you publish: the registry rejects a recipe whose coordinates disagree with the archive it arrives with.

kind toolchain, tool or package. It decides which table below is required.
name Lowercase: [a-z0-9][a-z0-9._-]*. It is a path segment and a directory.
version Opaque to the registry. Semver resolution is the client's job.
target A triple such as linux-x86_64, or any when not platform-specific.
The checksum is measured, never declared. A recipe has no sha256 or size key. Both are computed from the archive when it is published, so a recipe cannot claim a blob it does not describe.

Toolchains — kind = "toolchain"

A toolchain declares what it can compile. The tables mirror what pickup resolve --format toml already emits, so installing from this registry answers the same question probing does — without probing.

toolchain.vendor Required. clang, gcc, msvc — what the driver reports itself as.
toolchain.triple Required. The real target triple, e.g. x86_64-unknown-linux-gnu.
toolchain.c_driver Required. Path to the C driver inside the archive.
toolchain.cxx_driver Path to the C++ driver, when the toolchain ships one.
toolchain.c / .cxx std, compile_flags, link_flags, runtime_dirs, stdlib — the recipe pickup resolves to.
provides Capability ids the toolchain claims. Not validated against the catalogue, so a new id does not break publishing.
kind    = "toolchain"
name    = "clang"
version = "19.1.0"
target  = "linux-x86_64"

# Capability ids from pickup's catalogue. A declaration, not a proof:
# pickup may still probe the compiler before believing it.
provides = [
    "for_decl", "static_assert", "generic", "typeof",
    "attr_nodiscard", "constexpr", "nullptr", "native_bool",
    "auto_type", "lambda", "structured_bindings", "concepts",
]

[toolchain]
vendor     = "clang"
triple     = "x86_64-unknown-linux-gnu"
c_driver   = "bin/clang"      # relative to the root of the archive
cxx_driver = "bin/clang++"

[toolchain.c]
std           = ["c99", "c11", "c17", "c2x"]
compile_flags = []
link_flags    = []
runtime_dirs  = ["lib"]

[toolchain.cxx]
std           = ["c++11", "c++14", "c++17", "c++20"]
stdlib        = "libc++"
compile_flags = []
link_flags    = []
runtime_dirs  = ["lib"]

[about]
description = "LLVM C and C++ compiler, trimmed to what a build needs"
license     = "Apache-2.0 WITH LLVM-exception"
homepage    = "https://llvm.org"

Paths are relative to the root of the archive. A toolchain whose c_driver is bin/clang must unpack to a directory containing bin/clang, with no extra level in between.

Tools — kind = "tool"

A tool declares nothing about compiling, because nothing resolves against it. It is its own kind so that pickup resolve can never answer with a formatter, and so that a linker does not have to pretend to be a toolchain to be published.

tool.kind Required. formatter, linter, linker, language_server or plugin. The first three are what a build runs, the fourth is what an editor does, and the fifth extends molto itself.
tool.binary Required. The entry point inside the archive, which is not always the file: lld links only when invoked as ld.lld.
tool.aliases Other names in the archive that reach the same binary.
kind    = "tool"
name    = "clang-format"
version = "19.1.6"
target  = "linux-x86_64"

[tool]
kind   = "formatter"          # formatter | linter | linker | language_server | plugin
binary = "bin/clang-format"   # relative to the root of the archive

[about]
description = "LLVM's C and C++ formatter, the one molto fmt drives"
license     = "Apache-2.0 WITH LLVM-exception"

A tool that parses source needs the same builtin headers the compiler does, and carries its own copy: pickup installs tools apart from toolchains, so there is nothing next door to borrow from. clang-tidy is 134 MB for that reason and clang-format, which resolves no #include, is 4 MB.

Plugins — [tool] kind = "plugin"

A plugin extends molto rather than serving a build: it teaches it to read another build system, transforms what it is about to build, or produces something molto does not know how to produce. It is a tool because it travels the same way every other binary does; what makes it different is that molto runs it, which is why it declares what it will do and what it needs.

The permissions are the point. They are served with the catalogue, before a byte is downloaded, so that molto plugin install can show what it is about to allow and ask. That is also why a plugin recipe must declare schema = 2: a reader that predates plugins would ignore [plugin] as an unknown table and install an executable without ever seeing the list. At schema 2 it refuses the document instead.

plugin.capabilities Required, and never empty. One or more of frontend, transform, target, generator, compiler, linker, packager, command.
plugin.permissions What it may reach: ir.read, ir.write, project.read, project.write, filesystem.project, toolchain, process.spawn. An empty list is a plugin that asks for nothing.
plugin.extensions Filenames that select it as a frontend, such as meson.build. Empty for a plugin that is not one.
plugin.ir_schema The build IR schema it speaks, as an integer.
plugin.molto_min The oldest molto it works with.
schema  = 2                   # required: a plugin is unreadable at schema 1
kind    = "tool"
name    = "deb"
version = "1.2.0"
target  = "linux-x86_64"      # never "any": a plugin is a native executable

[tool]
kind   = "plugin"
binary = "bin/molto-deb"

[plugin]
capabilities = ["packager", "command"]
permissions  = ["project.read", "process.spawn"]
extensions   = []             # filenames that select it as a frontend
ir_schema    = 1
molto_min    = "0.17.0"

[about]
description = "Packages a molto build as a .deb"
license     = "Apache-2.0"

There is deliberately no permission for arbitrary network or filesystem access. A plugin that needs bytes from elsewhere declares a dependency, where the lock file and the checksum already are.

Packages — kind = "package"

A package declares what it needs to be compiled. The keys are the ones molto already understands in Project.toml, so a published package does not invent settings the build system cannot read.

package.std C standard the sources need, as molto passes it to -std.
package.include Include directories to add with -I, relative to the archive root.
package.link System libraries to link with -l.
package.defines Preprocessor defines, added with -D.
package.flags Passed verbatim to compiler and linker; never rewritten.
deps Dependencies, in the syntax molto's RFC-0003 specifies for Project.toml.
kind    = "package"
name    = "yyjson"
version = "0.10.0"
target  = "any"          # "any" for source packages and recipes

[package]
std     = "c17"
include = ["include"]    # -I, relative to the root of the archive
link    = ["m"]          # -l
defines = []             # -D
flags   = []             # passed verbatim

[deps]
# The syntax molto's RFC-0003 specifies for [deps] in Project.toml.
sqlite = "3.50.0"

[about]
description = "A high performance JSON library written in C"
license     = "MIT"

Source packages and recipes use target = "any". A package built per platform uses the same triples a toolchain does.

Publishing

From a directory holding the recipe and its archive, this is one command:

molto login
molto publish

Underneath it is two requests, in this order. PUT /v1/{kind}s/{name}/{version}/{target}/blob streams the archive into object storage, which verifies it hashes to x-molto-checksum as it lands. Then POST /v1/{kind}s, whose body is this file, records it.

The archive goes first and the row is written last — a catalogue entry without its blob is the one inconsistency the API cannot serve around. The recipe is stored whole, and comes back on GET /v1/{kind}s/{name}/{version}/{target} as metadata.

Archives are tar.zst. One archive per coordinate, holding the tree exactly as it should be unpacked. Trim it to what a build needs: a full LLVM release is around 735 MB on disk, of which a C build uses the driver and the builtin headers.

Withdrawing a version

A published coordinate is never overwritten and never deleted, so a publish that should not have happened is withdrawn instead: POST /v1/{kind}s/{name}/{version}/{target}/yank, and unyank to undo it. Both take the same bearer token publishing does.

A yank changes one column and no bytes. The download keeps working and keeps answering x-molto-yanked: true, so a lock file that already names the version keeps building; what changes is that a resolution starting now will not choose it.