Skip to content

Instantly share code, notes, and snippets.

@samoht
Last active January 14, 2026 20:30
Show Gist options
  • Select an option

  • Save samoht/338e352234bcc2183c2710feb4efc6fc to your computer and use it in GitHub Desktop.

Select an option

Save samoht/338e352234bcc2183c2710feb4efc6fc to your computer and use it in GitHub Desktop.
RFC: Developer Tools

RFC: Developer Tools

Mental Model

Dev tool = isolated build + simple cache index.

Tools like ocamlformat, odoc, and ocaml-lsp-server exist alongside your project without polluting its dependency tree.

dune fmt
    ↓
check index: ~/.cache/dune/index/ocamlformat.0.26.2-<checksum8>
    ↓ miss
solve + build in isolated context
    ↓
store binary in build cache (content-addressed)
    ↓
create index: index/ocamlformat.0.26.2-<checksum8> → db/files/v5/<hash>
    ↓
symlink: _build/install/default/bin/ocamlformat

User Experience

Just Works

dune fmt                    # Installs ocamlformat if needed
dune build @doc             # Installs odoc if needed

No configuration. Dune detects when a tool is required, solves dependencies, builds in isolation, and caches the result.

Version Pinning

For ocamlformat, create .ocamlformat:

version = 0.26.2

Explicit Commands

dune tools install ocamlformat       # Install + cache
dune tools exec odoc -- --help       # Run (auto-install if needed)
dune tools which ocamlformat         # Show location

Supported Tools

Tool Package Compiler-Dependent
ocamlformat ocamlformat No
odoc odoc Yes
ocaml-lsp-server ocaml-lsp-server Yes
utop utop Yes
merlin merlin Yes
odig odig Yes
ocaml-index ocaml-index Yes
dune-release dune-release No
opam-publish opam-publish No
earlybird earlybird No

Compiler-dependent tools interact with compiler internals. They must be built with the same OCaml version as the project.

Compiler-independent tools work with any compiler version.

Directory Structure

_build/
├── .locks/tools-ocamlformat/         # Lock directory
├── .pkgs/tools-ocamlformat/          # Package builds (isolated context)
└── install/
    ├── tools-ocamlformat/bin/        # Tool install prefix
    └── default/bin/ocamlformat       # Symlink to cached binary

~/.cache/dune/
├── db/files/v5/<hash>                # Build cache (content-addressed)
└── index/                            # Secondary index (simple keys)
    ├── ocamlformat.0.26.2-a1b2c3d4 → ../db/files/v5/<hash>
    └── odoc.2.4.0-f7e8d9c0 → ../db/files/v5/<hash>

Cache Design

Why a Secondary Index?

The build cache keys on rule hash (all inputs including compiler). For compiler-independent tools, this causes unnecessary cache misses:

Project A (OCaml 5.2) + ocamlformat 0.26.2 → rule hash X
Project B (OCaml 4.14) + ocamlformat 0.26.2 → rule hash Y

Different hashes, different cache entries - even though the binary is identical.

The tool index uses simpler keys that only include what matters:

Type Key Example
Compiler-independent <name>.<ver>-<checksum8> ocamlformat.0.26.2-a1b2c3d4
Compiler-dependent <name>.<ver>-<bid8> odoc.2.4.0-f7e8d9c0

For compiler-dependent tools, the build_id already includes the compiler as a dependency, so separate compiler version in the key is unnecessary.

No Duplication

The index is just symlinks into the content-addressed build cache. One copy of each binary, multiple ways to look it up.

Lookup Flow

  1. Compute index key from tool + version (+ build_id if compiler-dependent)
  2. Check ~/.cache/dune/index/<key>
  3. Hit: follow symlink to cached binary
  4. Miss: build tool, store in build cache, create index entry

Dependency Isolation

Each tool gets its own build context (tools-<name>). Tool dependencies are never added to the project's OCAMLPATH:

  • No version conflicts with project dependencies
  • No accidental imports of tool libraries
  • No constraint propagation from tools to project

Lazy Loading

Dev tools are loaded lazily: resolution and building happen only when the tool is actually needed.

dune build              # Does NOT resolve/build odoc
dune build @doc         # Resolves and builds odoc (if not cached)

This means:

  • dune build stays fast even with many dev tools configured
  • Network access for tool resolution only happens when tools are used
  • Compiler-dependent tools don't block builds until documentation/LSP is needed

The lazy loading applies to:

  • Dependency resolution (opam solve)
  • Source fetching
  • Building the tool

Once built and cached, subsequent uses are instant (symlink check only).

Eager Installation

For CI or pre-warming the cache:

dune install tools                    # Install all configured dev tools
dune install tools ocamlformat odoc   # Install specific tools

This is useful for:

  • CI pipelines that want deterministic tool availability
  • Pre-warming caches before going offline
  • Ensuring all tools are ready before starting work

Repository Resolution

Dev tools use the default repository defined for the workspace.

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment