Toolchains con pickup
pickup es quien responde qué compilador tiene esta máquina y qué puede demostrar — sus diez comandos, el TOML que hablan y qué le pide Molto.
Molto nunca elige un compilador. Tu manifiesto declara las capacidades que necesita el código, y
pickup responde qué binario local las proporciona. Es el gestor de toolchains del ecosistema
—rustup, pero para compiladores—: un programa aparte, con su propia línea de comandos, instalado al
lado de molto.
Responde con la verdad a una sola pregunta: ¿qué compiladores existen en esta máquina y qué sabe hacer de verdad cada uno? Y la responde compilando, nunca consultando una tabla de versiones. En una máquina Linux cualquiera:
| Compilador | Acepta -std=c2x |
Compila [[nodiscard]] |
|---|---|---|
| gcc 9 | sí | no |
| gcc 11 | sí | sí |
| gcc 12 | sí | sí |
Cualquier cosa que mire sólo el flag reporta gcc 9 como compilador de C23. Esa es la razón de que
este programa exista, y la razón de que un build que le pregunta sea portable de una forma en que
uno que nombra gcc-12 no lo es.
Los comandos
| Comando | Posicional | Opciones |
|---|---|---|
pickup list |
— | -f/--format <text|toml> |
pickup show |
<nombre> |
-f/--format |
pickup scan |
— | — |
pickup tools |
— | -f/--format |
pickup doctor |
— | -a/--all, -f/--format |
pickup resolve |
— | -l/--lang <c|c++>, -s/--std <nombre>, -r/--require <ids>, -v/--vendor <nombre>, --stdlib |
pickup search |
[nombre] |
-v/--version, --refresh, -f/--format |
pickup install |
<nombre>[@versión] |
-v/--version, --dry-run, --refresh |
pickup uninstall |
<toolchain> |
-y/--yes |
pickup default |
[toolchain] |
--clear, -f/--format |
Los diez están implementados; aquí nada responde «no implementado». Todo comando que produce datos
acepta --format text|toml, la respuesta va a stdout y el progreso y los avisos van a
stderr sin excepción: pickup list --format toml > toolchains.toml deja un archivo sin ninguna
barra de progreso dentro.
search e install aceptan cualquier nombre que publique el registry: una toolchain como clang o
gcc, o una herramienta como clang-format o clang-tidy.
El modelo
| Concepto | Qué es |
|---|---|
| Toolchain | Un driver con identidad resuelta: ruta, vendor, versión, triple de destino, su mitad de C++ y las features que demostró. Se llama vendor@versión[-etiqueta] |
| Feature | Una entrada de catálogo con un id estable y un programa que la demuestra: attr_nodiscard, constexpr, concepts |
| Receta | El compilador más los flags que lo hacen funcionar: compile_flags, link_flags, runtime_dirs, stdlib. Se encuentra probando hasta que uno compila, enlaza y ejecuta |
| Salud | Si el driver funciona de verdad: health_ok, no_driver, no_headers, no_link, no_run |
| Default | Una preferencia global, no una restricción, guardada en ~/.pickup/config.toml |
No hay concepto de canal —ni stable ni nightly— ni override por directorio: nada parecido a
rustup override, ni un archivo .pickup-toolchain. Lo que un proyecto necesita lo dice en
Project.toml.
Lo que tiene esta máquina
$ 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
Una toolchain se nombra por lo que es, no por el enlace que llevó hasta ella. Un mismo GCC
responde a cc, gcc-9, c89-gcc y g++-9; cuatro filas de eso dicen cuatro cosas donde hay una.
La identidad es el vendor y la versión. Cuando dos chocarían, el target lo decide —gcc@12.3.0-conda
junto al 12.3.0 del sistema—, y el sufijo sale del propio target, nunca de comparar toolchains entre
sí. SOURCE dice quién es responsable: pickup para la que instaló él y puede volver a quitar,
system para la que pertenece a la distribución.
Cualquier forma más corta del nombre vale allí donde se espera uno:
pickup show gcc@12.3.0-conda # exactamente esa
pickup show gcc@12.3.0 # la versión, sea cual sea el target
pickup show gcc@12 # la 12 más nueva
pickup show gcc # o gcc@latest
show es donde el probing se vuelve visible: imprime la identidad y luego cada feature, grupo a
grupo, con un yes o un no al lado. Cuando una consulta nombra de verdad a más de una, se enseña
igualmente la elegida y stderr dice qué otra cosa podía haber significado.
La primera consulta en una máquina prueba cada compilador una vez —y probar es hacer compilar a
todos los compiladores que hay—, así que dibuja una barra de progreso en stderr y la borra antes de
imprimir la respuesta. Las siguientes salen de la caché en milisegundos. pickup scan tira esa
caché y vuelve a probarlo todo, que es lo que hay que ejecutar tras instalar o actualizar un
compilador por otros medios.
Qué está mal en esta máquina
El catálogo de capacidades prueba el lenguaje y evita las cabeceras a propósito. Eso deja una
segunda pregunta, la que hace doctor: ¿puede este compilador producir un programa que se ejecute?
$ 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
✗ es lo que te deja sin poder hacer algo, y el código de salida sigue la misma regla. Un GCC
instalado sin su mitad de C++ está roto, pero en una máquina con cuatro toolchains que compilan C y
C++ no impide nada, así que no es un ✗. Conviene decirlo con todas las letras para la ejecución de
arriba: un formateador ausente sí es bloqueante, así que esa máquina hace que pickup doctor salga
con 1. En CI, esa es la diferencia entre una barrera y un informe.
Las líneas de resumen van primero y aparecen haya o no algo roto, porque qué tiene esta máquina se
pregunta mucho más a menudo que qué está roto. Nada se oculta en silencio: la cuenta del final dice
cuánto quedó fuera, y --all lo enseña con sus remedios. Un hallazgo es una causa, no un síntoma:
un GCC sin su mitad de C++ asoma en dos sitios que parecen no tener relación y se reporta una sola
vez.
Los remedios se nombran, nunca se aplican, con una excepción que queda del lado de pickup: un
archivo .cfg que él escribió para una toolchain que él instaló registra una decisión que nada
revalida, así que doctor la vuelve a poner al día y dice que lo hizo.
Pedir una
pickup resolve --require attr_nodiscard # features demostradas
pickup resolve --lang c++ --std c++20 # lenguaje y flag de estándar
pickup resolve --vendor gcc --format toml # para máquinas
--require y --std no son la misma pregunta. --require nombra features y se responde
probando: un compilador pasa sólo si compiló un programa que usa cada una. --std se responde con el
flag y nada más: pregunta si el driver puede invocarse en ese modo, que es lo que necesita quien va a
construir la línea de compilación. gcc 12 acepta -std=c2x y aun así falla cuatro de las features
atribuidas a C23. Pide las dos cosas cuando el código depende de las dos.
Cuando no hay nada que encaje, pickup nombra lo que le falta a cada candidato en vez de dejarte leer errores del compilador más tarde, y sale con 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
Las capacidades
Estos ids son exactamente lo que acepta [target].requires en el manifiesto. Cada uno es un programa
real compilado con -fsyntax-only por stdin —sin escribir archivos, y sin usar cabeceras salvo
spaceship—, porque la idea es probar el lenguaje y no la librería que tiene al lado.
| Grupo | 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 |
Un id desconocido es un error de uso que lo nombra, no una petición que en silencio no casa con nada.
Instalar una
Cuando la máquina no tiene nada adecuado, pickup trae una. Todo viene de un único registry, bajo coordenadas propias: un nombre, una versión y el target para el que se construyó.
pickup search # todos los catálogos que hay
pickup search clang # las versiones de uno, instalables aquí
pickup search clang --refresh # volver a preguntarle al registry
pickup install clang # la más nueva que se ofrezca
pickup install clang@19.1.6 # o --version 19.1.6; --version 19 significa cualquier 19
pickup install clang --dry-run # resolverlo sin descargar nada
Los catálogos se cachean una hora, porque search es de los comandos que la gente ejecuta dos veces
seguidas. Un nombre que nadie publica vuelve como una lista de lo que sí casa, no como un error: las
descargas van por curl -f, que reporta igual un 404 que una red caída, así que el nombre se resuelve
contra los catálogos ya descargados.
Nada se instala sin comprobarse. El registry declara un sha256 por artefacto, y uno que llega sin digest se descarta al leerlo, en vez de llevarlo hasta un punto donde algo decida si verificarlo. No hay flag que abra un camino sin verificar.
Nada se da por bueno por haberse descomprimido. Un compilador tiene que compilar, enlazar y ejecutar un programa antes de ser adoptado; una herramienta no tiene nada que compilar, así que lo que hace las veces es que el binario que el registry nombró responda. Lo que falle no deja nada instalado: el árbol se monta con un nombre temporal dentro del directorio donde acabará y se renombra en su sitio sólo cuando está completo, así que una instalación interrumpida no deja nada con apariencia de terminado.
Nada de lo que se publica sobre él se obedece. El registry dice con qué flags se construyó un artefacto; pickup los enseña y luego calcula los suyos. Esos metadatos describen la máquina que construyó el artefacto, y el flag que hace funcionar a un compilador aquí nombra un directorio de esta máquina.
Una versión que el registry ha retirado sigue listada y deja de ofrecerse: nombrar la versión entera la instala igualmente, porque algo ya construido con ella tiene que seguir siendo explicable.
Instalar necesita curl, tar y zstd en el PATH; todo se empaqueta en tar.zst, y doctor
reporta un zstd ausente como bloqueante porque sin él no se puede instalar nada en absoluto.
uninstall sólo quita lo que instaló pickup: lo que ya estaba en la máquina pertenece al gestor de
paquetes, y pickup lo dice en vez de tocarlo. Un nombre que casa con más de una se rechaza sin más y
las lista. La confirmación aparece donde hay alguien que pueda responderla, es decir, cuando stdin es
una terminal; en una tubería el borrado sigue adelante sin preguntar, con --yes o sin él.
Las herramientas con las que se trabaja un proyecto
Una máquina que compila no es una máquina en la que se pueda trabajar. clang-format y clang-tidy
vienen del registry por el mismo camino que un compilador, y aterrizan en ~/.pickup/tools/, aparte
de las toolchains, porque nada resuelve contra ellas.
$ 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 dice si falta alguna; tools dice cuáles hay y qué invocar. Sólo el TOML lleva la ruta,
porque es lo que necesita un build. cppcheck se detecta cuando lo trae el sistema y no se puede
instalar. Esta es la respuesta contra la que corren molto fmt y molto lint.
Elegir cuál gana
resolve responde con la toolchain más nueva que sea capaz de construir de verdad. default es cómo
se dice otra cosa:
$ pickup default gcc@12.3.0-conda
✓ Default toolchain is now gcc@12.3.0-conda
Es una preferencia, no una restricción. Pídele algo que no pueda servir y obtienes una respuesta
igualmente, más una línea en stderr diciendo por qué se descartó. Lo que se guarda es la identidad
resuelta, nunca lo que escribiste: gcc@latest registrado tal cual significaría en silencio un
compilador distinto la próxima vez que se instalara algo. pickup default a secas reporta la actual,
--clear la olvida, list la marca, y desinstalarla la borra. Vive en ~/.pickup/config.toml, al
lado de la caché y no dentro: vaciar la caché cuesta un rescan, y no debe costar una decisión.
Los flags son parte de la respuesta
Un compilador no es un comando; es un comando más todo lo que hay que decirle antes de que produzca un
programa que se ejecute. Por eso resolve publica la receta, demostrada igual que todo lo demás: los
candidatos se prueban en orden y el primero que compila, enlaza y ejecuta es el que se publica.
$ 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 añade una clave más, std_flag = "-std=c2x", para que quien llama pase el flag que pickup
verificó y no el que supuso. --lang c produce una tabla [c] en su lugar.
En qué librería estándar acaba depende de quién es dueño del compilador. Para uno que es del
sistema se prueba libstdc++ primero: es contra lo que se construyó todo lo demás de la máquina. Para
uno que instaló pickup es al revés —lo que la toolchain trajo dentro de su propio prefijo gana a lo
que le presta el sistema—, o un mismo pickup install sería un compilador distinto en cada máquina.
La consecuencia que conviene saber: el C++ construido con un Clang instalado usa libc++ y no enlazará
contra una librería de C++ del sistema construida sobre libstdc++. resolve --stdlib libstdc++ es el
camino de vuelta.
runtime_dirs existe porque enlazar no es ejecutar. Sin ese -rpath el enlazado sale con cero y el
programa muere con libc++.so.1: cannot open shared object file: una toolchain que pasa todas las
comprobaciones y no construye nada.
Qué le pide Molto
Molto ejecuta pickup como un proceso aparte y lee su TOML —pickup habla TOML porque Molto ya lo parsea, así que consumir el resolutor no añade ningún parser del otro lado.
pickup resolve --lang <c|c++> [--std X] [--require a,b] [--vendor v] --format toml
pickup tools --format toml
--std sale de [target].std, --require de [target].requires y --vendor sólo cuando el
manifiesto nombró un compilador. Molto cachea ambas respuestas en su propio .bin/wsdb y vuelve a
preguntar con --refresh-toolchain / --refresh-tools, o cuando la petición cambia: la pregunta se
hace una vez, no en cada build.
De la respuesta lee compiler.c_path (con path como alternativa), cxx_path, vendor y version.
Las tablas [c] y [cxx] se publican y todavía no se consumen del lado de Molto. Que un Clang
instalado funcione igualmente no es suerte: install deja la misma receta en un clang++.cfg junto
al driver, que Clang lee por su cuenta.
MOLTO_PICKUP apunta al binario cuando no está en el PATH. C_COMPILER y CPP_COMPILER se saltan
pickup por completo —y con él la verificación de [target].requires—, que es la razón de que Molto
imprima un aviso cuando se usan.
Códigos de salida
| Código | Significado |
|---|---|
| 0 | Éxito |
| 1 | La operación falló; doctor encontró algo bloqueante |
| 2 | Uso inválido: comando desconocido, id de feature desconocido |
| 3 | Ninguna toolchain satisface la petición |
| 4 | No implementado: reservado, hoy nadie lo devuelve |
El código 3 es distinto a propósito. «No hay nada que encaje» es una respuesta, no una avería, y
quien llama tiene que poder distinguirlas. default también devuelve 3 cuando la preferencia apunta
a algo que ya no está instalado.
Dónde vive todo
Todo está bajo $PICKUP_HOME, o ~/.pickup, así que instalar nunca pide permisos de administrador:
| Ruta | Contenido |
|---|---|
cache/ |
El inventario probado y los catálogos del registry, con TTL de una hora |
downloads/ |
Archivos en tránsito, eliminados una vez instalados |
toolchains/ |
Un directorio por toolchain instalada, <vendor>-<versión>-<target> |
tools/ |
El formateador y el linter, aparte |
config.toml |
Las decisiones del usuario: default y registry |
cache/ y downloads/ se pueden borrar en cualquier momento; el coste es un rescan, no una respuesta
equivocada. La caché del inventario se invalida por la ruta, la mtime y el tamaño de cada binario, y
por la huella del catálogo de capacidades: añadir una feature vuelve a probarlo todo.
Qué registry es de fiar lo deciden, en este orden, $PICKUP_REGISTRY_URL, luego registry = "…" en
~/.pickup/config.toml, y luego el valor compilado por defecto. Pickup lee esa clave y nunca la
escribe: un comando capaz de cambiarla sería un comando capaz de redirigir todas las instalaciones
futuras.
Plataforma
Linux en la práctica, POSIX por diseño, exactamente igual que Molto. Windows y macOS están en el roadmap; MSVC existe en el enum de vendors y en ningún otro sitio.