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. |
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.
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.