Skip to content
Molto

Toolchains with pickup

pickup is what answers which compiler this machine has and what it can prove — its ten commands, the TOML they speak, and what Molto asks for.

Molto never chooses a compiler. Your manifest states the capabilities the code needs, and pickup answers which local binary provides them. It is the toolchain manager of the ecosystem — rustup, for compilers — a separate program with its own command line, installed beside molto.

It answers one question truthfully: which compilers exist on this machine, and what can each of them actually do? It answers by compiling, never by consulting a version table. On one ordinary Linux box:

Compiler Accepts -std=c2x Compiles [[nodiscard]]
gcc 9 yes no
gcc 11 yes yes
gcc 12 yes yes

Anything reading the flag alone reports gcc 9 as a C23 compiler. That is the reason this program exists, and the reason a build that asks it is portable in a way one naming gcc-12 is not.

The commands

Command Positional Options
pickup list — -f/--format <text|toml>
pickup show <name> -f/--format
pickup scan — —
pickup tools — -f/--format
pickup doctor — -a/--all, -f/--format
pickup resolve — -l/--lang <c|c++>, -s/--std <name>, -r/--require <ids>, -v/--vendor <name>, --stdlib
pickup search [name] -v/--version, --refresh, -f/--format
pickup install <name>[@version] -v/--version, --dry-run, --refresh
pickup uninstall <toolchain> -y/--yes
pickup default [toolchain] --clear, -f/--format

All ten are implemented; nothing here answers “not implemented”. Every command that produces data takes --format text|toml, the answer goes to stdout, and progress and warnings go to stderr without exception — pickup list --format toml > toolchains.toml gets a file with no progress bar in it from anybody.

search and install take any name the registry publishes: a toolchain such as clang or gcc, or a tool such as clang-format or clang-tidy.

The model

Concept What it is
Toolchain A driver with a resolved identity: path, vendor, version, target triple, its C++ half, and the features it proved. Named vendor@version[-tag]
Feature A catalogue entry with a stable id and a program that proves it — attr_nodiscard, constexpr, concepts
Recipe The compiler plus the flags that make it work: compile_flags, link_flags, runtime_dirs, stdlib. Found by trying until one compiles, links and runs
Health Whether a driver actually works: health_ok, no_driver, no_headers, no_link, no_run
Default A global preference, not a restriction, kept in ~/.pickup/config.toml

There is no channel concept — no stable or nightly — and no per-directory override: nothing like rustup override, and no .pickup-toolchain file. What a project needs it says in Project.toml.

What this machine has

$ pickup list
NAME              VENDOR  VERSION  TARGET                    SOURCE
clang@22.1.8      clang   22.1.8   x86_64-unknown-linux-gnu  pickup
clang@14.0.0      clang   14.0.0   x86_64-pc-linux-gnu       system
gcc@12.3.0-conda  gcc     12.3.0   x86_64-conda-linux-gnu    pickup
gcc@12.3.0        gcc     12.3.0   x86_64-linux-gnu          system

A toolchain is named for what it is, not for the link that led to it. One GCC answers to cc, gcc-9, c89-gcc and g++-9; four rows of that say four things where there is one. The identity is the vendor and the version. Where two would collide the target settles it — gcc@12.3.0-conda next to the system’s own 12.3.0 — and the suffix comes from the target itself, never from comparing toolchains with each other. SOURCE says who is responsible: pickup for one it installed and can remove again, system for one that belongs to the distribution.

Any shorter form of the name works wherever one is expected:

pickup show gcc@12.3.0-conda   # exactly that one
pickup show gcc@12.3.0         # the version, whichever target
pickup show gcc@12             # the newest 12
pickup show gcc                # or gcc@latest

show is where the probing becomes visible: it prints the identity, then every feature, group by group, with a yes or a no against each. When a query genuinely names more than one, the chosen one is still shown and stderr says what else it could have meant.

The first query on a machine probes every compiler once — that is every compiler on it being made to compile, so it draws a progress bar to stderr and wipes it before printing the answer. Later queries come out of the cache in milliseconds. pickup scan throws that cache away and probes everything again, which is what to run after installing or upgrading a compiler by other means.

What is wrong with this machine

The capability catalogue tests the language and avoids headers on purpose. That leaves a second question, which doctor asks: can this compiler produce a program that runs?

$ pickup doctor
compilers
  ✓ 6 found - clang 14.0.0 to 22.1.8, gcc 9.5.0 to 12.3.0
  ✓ 4 of them build C and C++

tools
  ✗ formatter none found
      -> pickup install clang-format
  ✗ linter    none found
      -> pickup install clang-tidy

1 more thing worth knowing; pickup doctor --all

✗ is what leaves you unable to do something, and the exit code follows the same rule. A GCC installed without its C++ half is broken, but on a machine with four toolchains that build C and C++ it prevents nothing, so it is not a ✗. Worth spelling out for the run above: a missing formatter is blocking, so that machine makes pickup doctor exit 1. In CI that is the difference between a gate and a report.

The summary lines come first and appear whether or not anything is wrong, because what does this machine have is asked far more often than what is broken. Nothing is hidden silently: the count at the end says how much was left out, and --all shows it with its remedies. A finding is a cause, not a symptom — one GCC missing its C++ half shows up in two unrelated-looking places and is reported once.

Remedies are named, never applied, with one exception on pickup’s own side of the line: a .cfg file it wrote for a toolchain it installed records a decision nothing revalidates, so doctor puts it back in step and says it did.

Asking for one

pickup resolve --require attr_nodiscard        # proven features
pickup resolve --lang c++ --std c++20          # language and standard flag
pickup resolve --vendor gcc --format toml      # for machines

--require and --std are not the same question. --require names features and is answered by probing: a compiler passes only if a program using each one compiled. --std is answered by the flag alone — it asks whether the driver can be invoked in that mode, which is what a caller needs in order to build the compile line. gcc 12 accepts -std=c2x and still fails four of the features attributed to C23. Ask for both when the code depends on both.

When nothing matches, pickup names what each candidate lacks instead of leaving you to read compiler errors later, and exits 3:

$ pickup resolve --require constexpr,nullptr
pickup: no c compiler satisfies the request
  clang            (14.0.0) missing: constexpr nullptr
  gcc-12           (12.3.0) missing: constexpr nullptr

The capabilities

These ids are exactly what [target].requires takes in the manifest. Each is a real program compiled with -fsyntax-only through stdin — no files written, and no headers used except by spaceship, because the point is to test the language and not the library beside it.

Group Ids
C99 for_decl, line_comment, inline_fn
C11 static_assert, generic, noreturn
C23 attr_nodiscard, attr_maybe_unused, typeof, constexpr, nullptr, native_bool
C++11 auto_type, lambda, nullptr_cpp
C++17 structured_bindings, if_constexpr, fold_expressions
C++20 concepts, spaceship

An unknown id is a usage error naming it, not a request that quietly matches nothing.

Installing one

When the machine has nothing suitable, pickup fetches one. Everything comes from one registry, under coordinates of its own: a name, a version, and the target it was built for.

pickup search                        # every catalogue there is
pickup search clang                  # the versions of one, installable here
pickup search clang --refresh        # ask the registry again
pickup install clang                 # the newest offered
pickup install clang@19.1.6          # or --version 19.1.6; --version 19 means any 19
pickup install clang --dry-run       # resolve it, download nothing

Catalogues are cached for an hour, because search is the kind of command people run twice in a row. A name nobody publishes comes back as a list of what does match, not as an error — downloads go through curl -f, which reports a 404 and a dead network the same way, so the name is resolved against catalogues already fetched.

Nothing is installed without being checked. The registry states a sha256 for every artifact, and one arriving without a digest is dropped while reading rather than carried to a point where something decides whether to verify it. There is no flag that opens an unverified path.

Nothing is trusted for having unpacked. A compiler has to compile, link and run a program before it is adopted; a tool has nothing to compile, so what stands in for it is that the binary the registry named answers. Whatever fails leaves nothing installed — the tree is assembled under a temporary name inside the directory it will end up in and renamed into place only once complete, so an interrupted install leaves nothing that looks finished.

Nothing published about it is obeyed. The registry says which flags an artifact was built with; pickup shows them and then works out its own. Those metadata describe the machine that built it, and the flag that makes a compiler work here names a directory on this machine.

A version the registry has withdrawn stays listed and stops being offered — naming the whole version installs it anyway, because something already built from it has to stay explicable.

Installing needs curl, tar and zstd on the PATH; everything is packed tar.zst, and doctor reports a missing zstd as blocking because without it nothing installs at all.

uninstall removes only what pickup installed — one that was already on the machine belongs to the package manager, and pickup says so rather than touching it. A name matching more than one is refused outright and lists them. The confirmation prompt appears where there is somebody to answer it, meaning when stdin is a terminal; in a pipe the removal proceeds unasked, with or without --yes.

The tools a project is worked on with

A machine that compiles is not a machine that can be worked on. clang-format and clang-tidy come from the registry down the same path as a compiler, and land in ~/.pickup/tools/, apart from the toolchains, because nothing resolves against them.

$ pickup tools
KIND       NAME          VERSION                      SOURCE
formatter  clang-format  clang-format version 22.1.8  pickup
linter     clang-tidy    LLVM version 22.1.8          pickup

doctor says whether one is missing; tools says which are there and what to invoke. Only the TOML carries the path, because that is what a build needs. cppcheck is found when the system provides it and cannot be installed. This is the answer molto fmt and molto lint run against.

Choosing which one wins

resolve answers with the newest toolchain that can actually build. default is how you say otherwise:

$ pickup default gcc@12.3.0-conda
✓ Default toolchain is now gcc@12.3.0-conda

It is a preference, not a constraint. Ask for something it cannot serve and you get an answer anyway, plus a line on stderr saying why it was passed over. What gets stored is the resolved identity, never what you typed: gcc@latest recorded as written would quietly mean a different compiler the next time anything was installed. pickup default alone reports the current one, --clear forgets it, list marks it, and uninstalling it clears it. It lives in ~/.pickup/config.toml, beside the cache rather than inside it — clearing the cache costs a rescan, and must not cost a decision.

The flags are part of the answer

A compiler is not a command; it is a command plus whatever it has to be told before it produces a program that runs. So resolve publishes the recipe, proven the same way as everything else: candidates are tried in order and the first that compiles, links and runs is the one published.

$ pickup resolve --lang c++ --format toml
[compiler]
id = "clang@22.1.8"
path = "~/.pickup/toolchains/clang-22.1.8-x86_64-unknown-linux-gnu/bin/clang++"
c_path = "~/.pickup/toolchains/clang-22.1.8-x86_64-unknown-linux-gnu/bin/clang"
cxx_path = "~/.pickup/toolchains/clang-22.1.8-x86_64-unknown-linux-gnu/bin/clang++"
vendor = "clang"
version = "22.1.8"
target = "x86_64-unknown-linux-gnu"

[cxx]
stdlib = "libc++"
compile_flags = ["-stdlib=libc++", "--gcc-install-dir=/usr/lib/gcc/x86_64-linux-gnu/11"]
link_flags = ["-stdlib=libc++", "-Wl,-rpath,…"]
runtime_dirs = ["~/.pickup/toolchains/clang-22.1.8-x86_64-unknown-linux-gnu/lib/…"]

--std adds one more key, std_flag = "-std=c2x", so the caller passes the flag pickup verified rather than the one it assumed. --lang c produces a [c] table instead.

Which standard library it settles on depends on who owns the compiler. For one the host owns, libstdc++ is tried first: it is what everything else on the machine was built against. For one pickup installed it is the other way round — what the toolchain brought inside its own prefix beats what the host lends it, or a single pickup install would be a different compiler on every machine. The consequence worth knowing: C++ built with an installed Clang uses libc++ and will not link against a system-packaged C++ library built on libstdc++. resolve --stdlib libstdc++ is the way back.

runtime_dirs is there because linking is not running. Without that -rpath the link exits zero and the program dies on libc++.so.1: cannot open shared object file — a toolchain that passes every check and builds nothing.

What Molto asks for

Molto runs pickup as a separate process and reads its TOML — pickup speaks TOML because Molto already parses it, so consuming the resolver adds no parser on the other side.

pickup resolve --lang <c|c++> [--std X] [--require a,b] [--vendor v] --format toml
pickup tools --format toml

--std comes from [target].std, --require from [target].requires, and --vendor only when the manifest named a compiler. Molto caches both answers in its own .bin/wsdb and asks again on --refresh-toolchain / --refresh-tools, or when the request itself changes — the question is asked once, not on every build.

Of the answer it reads compiler.c_path (falling back to path), cxx_path, vendor and version. The [c] and [cxx] tables are published and not yet consumed on the Molto side. That an installed Clang works anyway is not luck: install leaves the same recipe in a clang++.cfg beside the driver, which Clang reads by itself.

MOLTO_PICKUP points at the binary when it is not on the PATH. C_COMPILER and CPP_COMPILER skip pickup entirely — and with it the verification of [target].requires, which is why Molto prints a warning when they are used.

Exit codes

Code Meaning
0 Success
1 The operation failed; doctor found something blocking
2 Invalid usage — unknown command, unknown feature id
3 No toolchain satisfies the request
4 Not implemented — reserved, nothing returns it today

Code 3 is distinct on purpose. “Nothing matches” is an answer, not a malfunction, and a caller must be able to tell them apart. default also returns 3 when the preference points at something no longer installed.

Where things live

Everything is under $PICKUP_HOME, or ~/.pickup, so installing never asks for administrator rights:

Path Contents
cache/ The probed inventory and the registry’s catalogues, TTL one hour
downloads/ Archives in flight, removed once installed
toolchains/ One directory per installed toolchain, <vendor>-<version>-<target>
tools/ The formatter and the linter, kept apart
config.toml The user’s own choices: default and registry

cache/ and downloads/ can be deleted at any time; the cost is a rescan, not a wrong answer. The inventory cache is invalidated by each binary’s path, mtime and size, and by the fingerprint of the capability catalogue — adding a feature reprobes everything.

Which registry to trust is decided by $PICKUP_REGISTRY_URL, then registry = "…" in ~/.pickup/config.toml, then the compiled-in default. Pickup reads that key and never writes it: a command able to change it would be a command able to redirect every future install.

Platform

Linux in practice, POSIX by design, exactly as Molto is. Windows and macOS are roadmap; MSVC exists in the vendor enum and nowhere else.