RFD0009 - Riot Toolchain System Snapshot
- Feature Name:
riot_toolchain_system_snapshot - Start Date:
2026-03-20 - Status:
implemented
Summary
Section titled “Summary”This RFD documents the current steady-state toolchain system used by riot.
It explains how toolchain configuration is loaded, how compiler bundles are
provisioned and selected, how target information flows through planning and
execution, and where the current implementation is broader or narrower than the
surface types suggest.
Motivation
Section titled “Motivation”The repository already has a high-level build-system snapshot in
RFD0003-riot-build-system-snapshot.md, but the toolchain portion of that
document is intentionally brief.
That leaves a gap for contributors trying to answer questions like:
- what exactly lives in a
riottoolchain - where toolchains are stored
- how
ocaml-toolchain.tomlchanges build behavior - how target triples affect planning and action execution
- how toolchains interact with the cache, sandbox, and formatter
- which parts of the model are active today and which parts are mostly scaffolding
This RFD exists to make the current behavior explicit without changing it.
Guide-level explanation
Section titled “Guide-level explanation”The current system treats a toolchain as a versioned, target-specific OCaml
bundle under ~/.riot/toolchains, plus a set of thin wrappers used by the rest
of riot.
At a high level:
riot-modelreadsocaml-toolchain.tomlfrom the workspace root.riot-toolchainresolves the requested version and target to a directory under~/.riot/toolchains/<version>/<target>.- The CLI ensures required toolchains are present before a build starts.
riot-buildholds a host toolchain in its local-session state.- A build worker may replace that host toolchain with a target-specific one
for an explicit
-xbuild. Build_ctxcarries host-vs-target information through planning.riot-planneruses that context to choose package target overrides and to shape action inputs.riot-executorruns planned actions in a sandbox and turns them intoocamldep, compiler, linker, formatter, and foreign-build subprocesses.
The main packages involved are:
riot-model: toolchain config, targets, build context, and directory conventionsriot-toolchain: toolchain discovery, provisioning, validation, and command wrappersriot-cli: user-facing target selection and install/list commandsriot-build: local-session toolchain ownership and per-build target selectionriot-planner: target-aware planning and compiler/link flag constructionriot-executor: sandboxed action executionriot-store: content-addressed storage for build outputs reused as includes and link inputs
Contributor mental model
Section titled “Contributor mental model”The current implementation has three separate but related notions:
- configured targets: the target triples listed in
ocaml-toolchain.toml - build context target: the
Build_ctx.targetvalue that drives target-aware planning - concrete compiler bundle: the
Riot_toolchain.tselected for the current build worker
Those usually line up during an explicit riot build -x <triple> invocation,
but they are not represented by one single type or initialization path.
End-to-end toolchain flow
Section titled “End-to-end toolchain flow”flowchart TD A[ocaml-toolchain.toml] --> B[Toolchain_config.from_workspace] B --> C[riot-cli target resolution] C --> D[ensure toolchain present] D --> E[Local_session / riot-build] E --> F[Build_ctx target selection] F --> G[riot-planner profile and target overrides] G --> H[action graph with includes and flags] H --> I[riot-executor sandbox] I --> J[riot-toolchain command wrappers] J --> K[ocamldep opt / compiler / formatter / foreign tools] K --> L[riot-store save and promote]What a contributor should expect
Section titled “What a contributor should expect”- A workspace-level
ocaml-toolchain.tomlcontrols the requested version and the set of known targets. riot toolchain listandriot toolchain installoperate on that workspace configuration.- A normal build defaults to the host triple.
- An explicit target build installs and validates the requested target
toolchain first, then runs planning and execution with a target-aware
Build_ctx. - Action execution happens in a sandbox, but dependency artifacts are usually
consumed from the content-addressed store by absolute
-Iinclude paths rather than being recopied in full. - The current CLI can resolve more than one configured target, but the actual build path still requires a single target per invocation.
Reference-level explanation
Section titled “Reference-level explanation”1. Configuration model
Section titled “1. Configuration model”The entrypoint for workspace toolchain configuration is
packages/riot-model/src/toolchain_config.ml.
Toolchain_config.t currently contains:
versionsourcetargets
Toolchain_config.from_workspace reads <workspace>/ocaml-toolchain.toml and
looks for:
[toolchain].version[toolchain].targets
version can be represented as:
- a string version like
"5.5.0" - a table with
path - a table with
url
If the file is missing or cannot be parsed, the system falls back to
Toolchain_config.default, which currently means:
- version
5.3.0 - source
Version "5.3.0" - no explicit targets, which means host-only
That is the effective workspace fallback used by the current runtime path, even
though packages/riot-toolchain/src/riot_toolchain.ml also defines a separate
default_ocaml_version constant with the value 5.5.0.
In this repository, the root ocaml-toolchain.toml overrides that fallback and
currently requests OCaml 5.5.0 plus several cross-compilation targets.
2. Filesystem layout
Section titled “2. Filesystem layout”packages/riot-model/src/riot_dirs.ml defines the main directory conventions.
The important ones for toolchains are:
~/.riot/toolchains/<version>/<target>~/.riot/projects/<project-id><workspace>/_build/<profile>/<target>/out<workspace>/_build/<profile>/<target>/sandboxThe toolchain package resolves binaries relative to:
~/.riot/toolchains/<version>/<target>/binThe current store root is created with Riot_dirs.cache_dir, which still uses a
host/debug-oriented default path helper. The store remains safe across targets
because stored entries are addressed by content hash, but the store directory
name itself is not yet fully target-aware.
3. What a Riot_toolchain.t contains
Section titled “3. What a Riot_toolchain.t contains”packages/riot-toolchain/src/riot_toolchain.ml defines the concrete toolchain
record.
A toolchain currently stores:
- requested version
- configured source metadata
- target triple string
- compiler wrapper
ocamlopt.optpathocamldep.optwrapperocamlformatwrapper
The command wrappers are thin. They mainly:
- remember the binary path
- convert planner data into command-line arguments
- shell out with
Command.output
Despite the Ocamlc module name, the current toolchain record points that
wrapper at ocamlopt.opt. In steady state, the build path currently drives the
native compiler binary rather than switching between separate bytecode and
native compiler executables.
4. Host triple and target resolution
Section titled “4. Host triple and target resolution”Riot_toolchain.get_host_triple derives the host triple from
System.host_triplet on Unix and otherwise falls back to
x86_64-unknown-linux.
Toolchain paths are resolved as:
- host/default:
~/.riot/toolchains/<version>/<host-triple> - explicit target:
~/.riot/toolchains/<version>/<target>
riot-toolchain distinguishes between:
init: initialize the host toolchaininit_for_target: initialize a specific target toolchainget_for_target: alias forinit_for_target
list_toolchains computes the set of relevant targets from config:
- if
targets = [], it lists only the host triple - otherwise it lists the configured targets exactly
5. Provisioning and validation
Section titled “5. Provisioning and validation”Provisioning order is implemented in riot_toolchain.ml.
For the host toolchain, initialization works like this:
- look for
~/.riot/toolchains/<version>/<host>/bin - if present, validate required binaries
- otherwise, look for
./ocaml/compiler - if
./ocaml/compilerexists, symlink it into the expected~/.riot/toolchains/...location - otherwise, download a prebuilt tarball from
https://cdn.ocaml.ai/ocaml/
For explicit non-host targets, the local ./ocaml/compiler shortcut is not
used. The code goes directly to the target-specific install-or-download path.
The current download URL patterns are:
- native:
ocaml-<version>-<host>.tar.gz - cross:
ocaml-<version>-<host>-x-<target>.tar.gz
Validation currently checks for the presence of:
- the toolchain record’s compiler-wrapper path
ocamlopt.optocamldep.opt
check_health then runs the compiler binary with -version.
ocamlformat is part of the toolchain record, but it is not currently part of
the binary-existence check.
5.1 Download provenance and packaging boundary
Section titled “5.1 Download provenance and packaging boundary”The current repository clearly defines the consumer side of prebuilt toolchains.
The download entrypoints are:
packages/riot-toolchain/src/riot_toolchain.mlfor steady-stateriotbootstrap.pyfor bootstrap-time toolchain acquisition
Both point at:
https://cdn.ocaml.ai/ocaml/The tarball naming convention expected by this repository is:
ocaml-<version>-<host>.tar.gzocaml-<version>-<host>-x-<target>.tar.gz
From the extraction code, the archive is expected to unpack directly into the
target toolchain directory rather than into an extra nested top-level folder.
That is, after extraction, ~/.riot/toolchains/<version>/<target>/ is expected
to contain paths such as:
bin/ocamlopt.optbin/ocamldep.optbin/ocamlformatlib/ocaml/...
This direct-layout expectation is an inference from the current extraction code
in bootstrap.py and riot_toolchain.ml.
What this repository does not currently define is the producer side of those tarballs.
The repository now includes a local manual helper for the producer side of those tarballs:
scripts/toolchain/ocaml.sh
That script can build selected vendored compilers, package them with the
existing vendor/ocaml/cross/package.sh naming convention, upload existing
tarballs, or do both in one release step against an S3-compatible bucket such
as Cloudflare R2.
What this repository still does not currently define is an active automated publisher for those tarballs.
The disabled workflow reference remains:
- a GitHub Actions workflow under
.github/workflows/ocaml-publish-toolchains.yml.disabled - no manifest describing which external system is responsible for publishing them
By contrast, this repository now includes local manual helpers for publishing:
riotrelease tarballs viascripts/release/riot.sh- the top-level install script at
cdn.ocaml.ai/riot/install.sh - prebuilt OCaml tarballs via
scripts/toolchain/ocaml.sh
The disabled workflow references remain:
riotrelease tarballs undercdn.ocaml.ai/riot- the install script upload job in
.github/workflows/release.yml.disabled - the OCaml tarball upload jobs in
.github/workflows/ocaml-publish-toolchains.yml.disabled - the
ghcr.io/leostera/riot/riot-builderDocker image
So the current architectural boundary is:
- this repo consumes prebuilt OCaml toolchain tarballs from
cdn.ocaml.ai/ocaml - this repo can fall back to building OCaml from source during bootstrap via
riot-ocaml - this repo does not currently document or automate the publication of those OCaml tarballs themselves
The Docker builder image is related but separate. docker/Dockerfile bootstraps
or downloads toolchains during the image build, then copies
/root/.riot/toolchains into the final riot-builder image so containerized
builds start with a pre-seeded toolchain cache. That image pipeline does not
publish the OCaml tarballs to cdn.ocaml.ai/ocaml.
6. CLI surface
Section titled “6. CLI surface”The CLI entrypoints live in:
packages/riot-cli/src/cli.mlpackages/riot-cli/src/build.mlpackages/riot-cli/src/toolchain_cmd.ml
There are two main user-facing toolchain surfaces.
6.1 riot toolchain
Section titled “6.1 riot toolchain”riot toolchain list:
- loads
Toolchain_configfrom the workspace - reports configured targets
- marks each one as installed, not installed, or incomplete
riot toolchain install:
- loads the same config
- installs all missing or incomplete toolchains
6.2 riot build
Section titled “6.2 riot build”riot build supports:
-x/--target <pattern>--all-targets
Target resolution happens in build.ml:
hostandnativemap to the current host tripleallmaps to all configured targets- exact configured target names are accepted directly
- other strings are treated as substring matches against the configured target list
The CLI then:
- installs any missing toolchains for the resolved target set
- validates the selected target toolchain with
init_for_target - passes
target_archinto the local session build request
The current implementation still executes only one target build at a time. If a pattern matches multiple targets, the CLI stops and asks the user to choose a single target explicitly.
cli.ml also runs a host-toolchain preflight for build, run, test,
bench, and fmt before starting the local session. That means toolchain
initialization logic currently exists both in the top-level CLI path and again
inside the build worker.
7. Local session and build worker behavior
Section titled “7. Local session and build worker behavior”packages/riot-build/src/internal_server.ml creates server state for a single
command invocation.
That state includes:
- the rescanned workspace
- a host toolchain
- the artifact store
- the package graph
The host toolchain is initialized during server startup with:
Toolchain_config.from_workspaceRiot_toolchain.init
The build request protocol then carries:
- package target selection (
Allor a package name) target_arch : string optionsession_id
The actual target-aware selection happens in
packages/riot-build/src/build_server.ml.
The build worker:
- starts from the workspace debug profile plus workspace profile overrides
- parses
target_arch, if present, intoSystem.Host.t - creates a
Target.HostorTarget.Crossvalue - builds
Build_ctx - initializes the concrete toolchain for the explicit target
- falls back to the host toolchain if target-specific initialization fails
- invokes
Coordinator2.build_workspace
This is the main runtime seam between:
- the symbolic target carried by
Build_ctx - the concrete compiler bundle carried by
Riot_toolchain.t
8. Cross-compilation metadata
Section titled “8. Cross-compilation metadata”Cross-compilation metadata lives in:
packages/riot-model/src/target.mlpackages/riot-toolchain/src/cross_compiling_toolchain.ml
When the build worker sees a non-host target, it calls
CrossCompilingToolchain.detect.
That detection step:
- derives a compiler prefix from the target triple
- looks for
<prefix>gccinPATH - asks that compiler for
-print-sysroot - records
sysroot,bin_dir,bin_prefix, andc_compiler
The result is embedded into Target.Cross, which means the build context can
carry cross-compilation metadata even though the OCaml compiler bundle itself is
still resolved separately by Riot_toolchain.init_for_target.
Today this metadata is primarily consumed to thread --sysroot=... into C
compile and link flags.
9. Planning-time toolchain behavior
Section titled “9. Planning-time toolchain behavior”The planner uses toolchain information in three different ways.
9.1 Package-level hashing and profile resolution
Section titled “9.1 Package-level hashing and profile resolution”packages/riot-planner/src/package_planner.ml resolves the effective profile
for each package by:
- starting from the build-context profile
- applying package profile overrides by profile name
- applying package target overrides by target platform name
The target-override key space is currently platform-oriented:
macoslinuxwindows
It is not keyed by full target triple.
The fast-path package input hash includes:
Build_ctx- package metadata
- workspace-specific dependency details
- dependency hashes
It excludes:
Session_id- module-graph shape
- action-graph shape
9.2 Module dependency discovery
Section titled “9.2 Module dependency discovery”packages/riot-planner/src/module_graph.ml uses the toolchain’s ocamldep
wrapper to wire OCaml module dependencies.
Important properties:
ocamldepruns in batch mode- files are sorted deterministically before invocation
- dependency results are resolved back into package namespaces
9.3 Action-node hashing
Section titled “9.3 Action-node hashing”packages/riot-planner/src/action_node.ml includes the toolchain hash in each
action-node hash.
That means action nodes are explicitly sensitive to compiler path changes.
This is stricter than the package fast-path hash, which currently does not add the resolved toolchain hash directly. The current system therefore has a real distinction between:
- package-level cache admission
- action-node identity inside a fully planned package
10. Action generation
Section titled “10. Action generation”packages/riot-planner/src/action_graph.ml is where toolchain-adjacent planner
decisions become executable action data.
The important behaviors are:
- OCaml compile actions receive include paths and explicit compiler flags such as
-open,-nopervasives, and optionally-nostdlib - dependency cache directories from
riot-storeare passed as-Iincludes - special OCaml stdlib package includes like
+unixand+dynlinkare passed through untouched - C compile actions receive
profile.cc_flags - executable and shared-library link actions receive:
- the current package
ld_flags - target-specific dependency
ld_flagscollected transitively from the dependency set --sysroot=...when the build context is cross-compiling
- the current package
- foreign dependencies become explicit
BuildForeignDependencyactions
This is also where target-aware package overrides show up concretely. The build-context target affects:
- which package target override is selected
- whether sysroot is threaded into C compile and link commands
11. Sandbox execution
Section titled “11. Sandbox execution”packages/riot-executor/src/sandbox.ml and
packages/riot-executor/src/parallel_action_executor.ml own the execution side.
For each package build:
- a fresh sandbox directory is created under
_build/.../sandbox - package source inputs are copied into that sandbox
- dependency
.ofiles are copied in from cached artifacts - other dependency artifacts stay in the immutable store and are referenced by absolute include paths
- planned actions are executed in dependency order
The action executor translates planner actions into riot-toolchain calls.
Relative includes become sandbox paths.
Absolute cache include paths stay absolute.
+unix and +dynlink are preserved as OCaml special include paths.
This is the main point where the toolchain wrappers become real subprocesses:
Ocamldep.batch_depsOcamlc.compile_interfaceOcamlc.compile_implOcamlc.generate_interfaceOcamlc.compile_cOcamlc.create_libraryOcamlc.create_executableOcamlc.create_shared_library
Foreign dependencies are the exception. They run their own build_cmd directly
from the foreign package directory instead of going through the OCaml toolchain
wrapper.
12. Store interaction
Section titled “12. Store interaction”packages/riot-store/src/store.ml provides the content-addressed artifact
store.
Toolchain behavior matters to the store in two ways.
First, cache admission happens at package granularity. If the package hash
already exists, Package_builder.build skips full action execution and promotes
the stored outputs into the target output directory.
Second, dependency packages are reused structurally:
Store.get_artifact_dirprovides include directories for downstream compilationStore.get_artifact_pathsprovides object files copied into the sandbox for downstream linking
That means the store is not just a final output cache. It is also part of the toolchain input surface for downstream packages.
13. Formatter behavior
Section titled “13. Formatter behavior”The toolchain package also owns ocamlformat.
packages/riot-toolchain/src/ocamlformat.ml shells out to the toolchain’s
ocamlformat binary for:
- formatting files in place or in check mode
- formatting ad hoc code snippets
riot-build exposes format requests over the same local-session protocol used
for builds, and riot-cli performs the same host-toolchain preflight before
reaching that path.
14. Current asymmetries and sharp edges
Section titled “14. Current asymmetries and sharp edges”This section is descriptive. It captures the current system shape that a reader will encounter in the code.
14.1 source is broader than active initialization
Section titled “14.1 source is broader than active initialization”Toolchain_config.source can be:
VersionPathUrl
The current initialization path preserves that source metadata in the toolchain
record, but still resolves binaries from the standard
~/.riot/toolchains/<version>/<target> layout.
In other words, Path and Url are modeled in config today, but the active
steady-state load path is still centered on the canonical ~/.riot/toolchains
directory layout.
14.2 The compiler wrapper is more native-oriented than its names suggest
Section titled “14.2 The compiler wrapper is more native-oriented than its names suggest”The command wrapper module is named Ocamlc, and Profile.kind models
bytecode-vs-native intent.
The current concrete toolchain record still wires the compiler wrapper to
ocamlopt.opt, and the action graph always emits native-shaped implementation
outputs such as .cmx.
That means the current steady-state build path is more native-oriented than the surface type names alone would imply.
14.3 Profile data is richer than the current command layer
Section titled “14.3 Profile data is richer than the current command layer”Profile.t models:
- kind
- optimization toggles
- warnings
- errors
- raw compiler flags
- C flags
- linker flags
The current action-generation path actively consumes:
- target/platform selection
cc_flagsld_flags
Other profile fields are present in the model and hashing story, but are not yet fully translated into command-line generation in the current planner/executor path.
14.4 Toolchain initialization is duplicated across layers
Section titled “14.4 Toolchain initialization is duplicated across layers”The CLI does an early host-toolchain preflight. The local server also initializes a host toolchain. The build worker may then initialize a target-specific toolchain again.
That duplication is part of the current implementation contract.
14.5 Multi-target configuration is ahead of multi-target execution
Section titled “14.5 Multi-target configuration is ahead of multi-target execution”The workspace config can name many targets. The CLI can list and install many targets. Target pattern resolution can produce many matches.
The actual build path still executes one target per invocation.
Drawbacks
Section titled “Drawbacks”- The toolchain system is spread across model, CLI, server, planner, executor, and store code rather than being concentrated in one package.
- The public type surface suggests a more general source-selection and profile-selection model than the currently exercised execution path provides.
- Host-toolchain startup, target-toolchain startup, and cross-compilation metadata detection happen in separate layers.
Rationale and alternatives
Section titled “Rationale and alternatives”This document is descriptive, not prescriptive.
Alternatives considered:
- expanding
RFD0003instead of writing a dedicated toolchain snapshot - relying only on package-local
AGENTS.mdfiles - documenting only the user-facing CLI behavior and not the implementation
Those alternatives were weaker for the current need. The toolchain system spans enough packages and enough subtle runtime behavior that a standalone snapshot is easier to maintain and easier to reread later.
Prior art
Section titled “Prior art”The main prior art for this RFD is the current implementation across:
riot-modelriot-toolchainriot-cliriot-buildriot-plannerriot-executorriot-store
RFD0002-riot-bootstrap.md is also related, but it describes the bootstrap
path for acquiring the first working riot, not the steady-state runtime path
once riot is already available.
Unresolved questions
Section titled “Unresolved questions”- Should
Toolchain_config.sourcebecome an active binary-resolution input instead of mostly metadata? - Should the command layer expose separate bytecode/native compiler selection more directly?
- Should package-level cache admission incorporate the resolved toolchain hash explicitly?
- Should the store root become fully target-aware at the directory-convention level instead of relying only on content hashes?
- How much of the current CLI/server/build-worker initialization duplication should remain?
Future possibilities
Section titled “Future possibilities”- unify toolchain initialization behind one runtime path
- make multi-target builds a first-class execution mode instead of a configuration-only capability
- thread more of
Profile.tinto concrete compiler invocation - make target-aware cache and output directory conventions more consistent
- separate native and bytecode toolchain concerns more clearly if the build system starts exercising both modes explicitly