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 or linker — the vocabulary pickup tools already emits.
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
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.

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.