Skip to content
Molto

The commands

Every subcommand, the flags it takes, the exit code it returns and what it prints to a terminal or a pipe.

Every command finds your project by walking up from the current directory, so all of them work from any subdirectory.

Command Options
molto new <name> —
molto init —
molto build -p/--profile, --refresh-toolchain, -j/--jobs
molto run [-- args] -p/--profile, --refresh-toolchain, -j/--jobs
molto test -p/--profile, --refresh-toolchain, -j/--jobs
molto clean -a/--all
molto fmt -c/--check, -d/--diff, --refresh-tools, --refresh-analysis, -j
molto lint -p/--profile, -f/--format, --refresh-toolchain, --refresh-tools, --refresh-analysis, -j
molto add <dep>[@<version>] --dev, --git, --path, --archive, --registry
molto remove <dep> —
molto metadata -o/--output, --include-dev
molto login -r/--registry, -e/--email, -t/--token
molto publish --recipe, -f/--file, --dry-run

molto bench, molto migrate and molto update exist in the CLI and exit 5: the command is recognised, the behaviour is not implemented. update stays that way on purpose — molto add <name> is the upgrade, the way npm install is.

The parser accepts --opt value, --opt=value, -o value and boolean flags. Everything after -- is forwarded to the program under molto run. --help and --version come for free.

Starting a project

molto new my_app   # a new directory with a manifest, src/, include/, tests/ and a .gitignore
molto init         # the same, in the directory you are already in

Neither ever overwrites a file that already exists, and neither initialises git.

Building and running

molto build                 # into build/debug/
molto build -p release      # into build/release/
molto run -- --flag value   # build, run, and forward everything after --
molto test                  # one binary per file under tests/, by default

molto run propagates the program’s own exit code verbatim, and reports 128 + N when it dies from signal N. That is the one command whose exit code is not Molto’s.

-j <n> caps the worker pool; absent means every core. It is refused unless it is a count between 1 and 1024, and it is never recorded or fingerprinted — two builds differing only in -j produce the same objects.

Output layout

Path Contents
build/<profile>/<package> The executable
build/<profile>/obj/src/**/*.c.o Intermediate objects
build/<profile>/tests/<name> One binary per test file (per_file)
build/<profile>/tests/<package>_tests The single test binary (single)
.bin/wsdb Incremental state and the cached toolchain answer
compile_commands.json The compilation database, at the project root
Molto.lock The resolved dependency graph

build/ and .bin/ belong to Molto and are gitignored. Molto.lock is generated and committed. molto clean removes build/; molto clean --all also removes .bin/. Neither touches ~/.molto.

compile_commands.json is written by a build, and the last one wins: molto build covers src/ plus dependencies, molto test covers those and tests/, and -p release leaves a database describing release. fmt and lint write none. It is written even when the build fails.

Exit codes

Code Meaning
0 Success
1 Build failure; fmt --check found unformatted files; lint produced an error
2 Invalid manifest or invalid configuration
3 Dependency failure
4 Usage error
5 The command exists but is not implemented

A lint warning is reported and still exits 0. Only error severity fails the command.

What a build prints

Everything goes to stderr, so molto build > log still shows the build and molto run > out captures only your program.

On a terminal it draws an inventory, then a live region, and takes the region away when the work stops:

 ◆ registry   sqlite3 v3.50.3
 ◇ modules    network
 ● project    62 files
 ○ cached     20 files

 ● project    services/build_service.c
 ◆ sqlite3    btree.c
 … and 5 more
 ████████░░░░░░░░  22%  47/210

A dependency gets one line however many sources it has — a package is one piece of work. Nothing already up to date is listed; it is counted on cached, so a no-op rebuild is three lines and no region.

Redirect stderr — a pipe, a file, a CI job — and you get one line per source, no region, no bar, and not one escape sequence. That is the full record, and it is what CI should keep. NO_COLOR=1 removes the colour on a terminal but not the region, which is about motion. The cursor is never hidden, so Ctrl-C leaves the terminal as it found it.

A failing unit is reported in a frame carrying the source line, a caret under the character the tool named, and a footer saying whose code it was. A whole diagnostic is composed and handed to the report in one call, so two workers can never interleave. A build that fails prints no Finished line.

Environment variables

Variable Effect
MOLTO_PICKUP Path to the pickup binary when it is not on the PATH
C_COMPILER Forces the C driver — skips pickup and skips requires verification
CPP_COMPILER Forces the C++ driver, same caveat
MOLTO_CLANG_FORMAT Path to the formatter; bypasses resolution, not cached
MOLTO_CLANG_TIDY Path to the linter; same
MOLTO_CACHE Root of the shared dependency cache; ~/.molto/cache otherwise
NO_COLOR Honoured by the test runner

Molto prints a warning to stderr when C_COMPILER is used, because a compiler chosen by hand was never checked against [target].requires.

Where pickup comes in

Molto does not choose a compiler. A manifest states the capabilities the code needs; pickup answers which local binary provides them, and the answer is cached in the workspace database — the question is asked once rather than on every build.

pickup list       # every toolchain on this machine
pickup doctor     # what stops this machine from building, and what fixes it
pickup install clang
pickup tools      # the formatter and the linter, and where they are

If pickup resolve reports a runtime_dirs for a toolchain it installed, what Molto links carries the -rpath in itself, so a binary keeps running wherever it goes.

Platform

POSIX only, deliberately: fork/exec, flock and sysconf are used directly, and CI runs Ubuntu. Windows and macOS were promised for 0.2, moved to 0.4, and stay parked until the ecosystem is deep enough on one platform to be worth porting to three.