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.