Dependencies
Exact versions, the two dependency tables, recipe.toml, Molto.lock and the caches behind them.
[deps]
sqlite = "3.53.4" # a registry coordinate, at exactly that version
http = { path = "modules/http" } # a directory you are working on
[dev-deps]
tinytest = { path = "modules/tinytest" } # only ever compiled into tests/
molto add sqlite # the newest release, written into the manifest as exact
molto add sqlite@3.53.4 # that one
molto add tinytest --dev --path ../tt # into [dev-deps]
molto remove sqlite
Four rules carry most of the behaviour
Versions are exact. ^, ~ and >= are refused, not interpreted. A range would let a
release nobody read enter a build without a diff.
One version per package, across both tables at once. A test binary links src/ objects
together with test objects, and two versions there are duplicate symbols.
Dev dependencies are not transitive, and their include directories are placed only on the
command line that compiles tests/. A src/ file that includes one fails to compile, on the
first build. That is the enforcement, not a documented convention.
Both tables fail closed. An unknown key, two sources, a range, or a key that is specified but
unimplemented (recipe, artifact, optional, features) is an error naming the dependency —
the opposite of the rest of the manifest, which drops what it does not know.
Declaring one
[deps]
yyjson = "1.2.32" # shorthand for { version = … }
sqlite = { git = "https://github.com/sqlite/sqlite", tag = "3.50.0" }
http = { path = "modules/http" }
zlib = { archive = "https://…", sha256 = "…" }
fast = { version = "2.0.0", registry = "myorg" }
[registries]
myorg = "https://registry.example.com"
Source keys, exactly one required: version, git, path or archive. Modifiers:
branch / tag / rev alongside git and at most one of them, sha256 required with
archive, and registry which must be declared in [registries].
The flags of molto add spell the same keys, so what you type and what lands in the manifest
match. add and remove edit lines rather than rewriting the file: comments, alignment and key
order survive, re-adding a name at a new version replaces it in place, and any edit that would
leave an unreadable manifest is refused before the file is touched.
recipe.toml
A dependency describes itself with a recipe at the root of its source. A path or git
dependency without one is refused with exactly that message.
schema = 1
form = "source"
kind = "package"
name = "sqlite"
version = "3.53.4"
target = "any"
[source]
archive = "https://sqlite.org/2025/sqlite-amalgamation-3530400.zip"
sha256 = "…"
compression_format = "zip" # zip | tar | tar.gz | tar.bz2 | tar.xz | tar.zst
strip_prefix = "sqlite-amalgamation-3530400"
[build]
system = "none" # make | cmake | autotools | meson | none
[artifacts]
type = "source" # source | static | shared
sources = ["sqlite3.c"]
exclude = ["shell.c"]
include = ["."]
link = ["m", "pthread"]
defines = ["SQLITE_THREADSAFE=1"]
Only [artifacts] type = "source" can be consumed today. Molto compiles the drop as its own
translation units, merging the recipe’s includes, defines, flags and links into the build.
static and shared are refused with a message, and so is any [build] system other than
none: no build system is ever run. Nothing in Molto invokes make, cmake or meson.
A recipe never carries a script. Not for unpacking, not for building. That is why
compression_format names a bounded set and [build] names a system rather than a command line —
a recipe that could run a string would make every dependency a remote execution.
compression_format is declared, never inferred: a URL may end in .zip and serve a tarball.
Absent means infer from the extension, for recipes written before the key existed.
[about] is where a dependency’s licence comes from, and molto metadata
reports exactly what it finds there. A recipe declaring none produces a component with no
licenses field — silence rather than a claim.
Resolution
- Read
[deps],[dev-deps], andMolto.lockif it is there. - For a coordinate, ask the registry once per name. Without
@<version>,molto addpicks the newest by semver precedence, not string order, and writes that number into the manifest. - Fetch what is not cached, walk each recipe’s own
[deps], and repeat. - Refuse a resolution the lock disagrees with; write
Molto.lockotherwise.
Two versions of one package is your decision, not Molto’s. There is no range to widen and no highest version to pick, so the message’s whole job is to say who asked for what:
molto: sqlite is required at two versions
3.50.3 ← your Project.toml
3.53.4 ← required by http 1.2.0
Molto.lock
Generated, committed, sorted by name, every list ordered — so its diff is worth reading, which is the whole point of committing it.
version = 1
root = "app"
[[package]]
name = "a"
version = "1.0.0"
source = "registry+https://…"
checksum = "…" # omitted for a path dependency, never written empty
scopes = ["runtime", "dev"]
dependencies = ["b"]
A lock that is absent, unreadable, not valid TOML, or from a newer format is treated identically: resolve again. A lock whose direct dependencies no longer match the manifest is stale, and re-resolved rather than patched.
The caches
| Path | What |
|---|---|
~/.molto/cache/sources/<name>/<version>/<target> |
A fetched tree, once per coordinate, stamped on completion |
~/.molto/cache/objects |
Objects compiled from dependencies, keyed by the full compile command |
~/.molto/credentials.toml |
The registry token, mode 0600 |
$MOLTO_CACHE relocates the first two. A directory without its stamp is the remains of an
interrupted fetch: removed, not read.
The object cache covers dependencies only. Reusing an object requires knowing that the source
and every header it includes are identical, and for a dependency the coordinate answers for the
whole immutable tree. A project’s own sources are mutable and their header graph is unknown until
the compiler has run — that is ccache’s problem, and Molto does not try to be one. A path
dependency is excluded for the same reason: its bytes are whatever is on disk right now.
Fetching shells out to curl, tar, unzip and git — the same reasoning as shelling out to a
compiler rather than linking one.
The registry
Reads are public; only publishing needs a token. molto login exchanges email and password for
one, or stores one pasted from the account page with --token. The token never appears in argv:
ps is world-readable, so the header goes through a curl config file created 0600 and deleted
afterwards.
molto publish sends the blob first and the row last. --dry-run reads and hashes without
sending, which is the only way to check a recipe — a published coordinate is immutable.