blog :: Post "intro-to-opam"

Introduction to opam

2026-07-19 · 8 min

opam installs OCaml compilers and OCaml packages. Dune builds your project; opam supplies the compiler and the libraries that project depends on. This is most of the workflow, command by command.

§ 01

A little history

opam came out of OCamlPro in 2012, replacing GODI and a lot of hand-rolled installation. Its distinguishing idea is the switch: a self-contained prefix holding one compiler and one set of packages, so two projects on different compiler versions never collide. The package descriptions live in a git repository (opam-repository) that anyone can open a pull request against. Under the hood a constraint solver picks a version assignment that satisfies every package's declared bounds at once.

§ 02

Install and initialize

Install the binary. On macOS via Homebrew, on Debian/Ubuntu via apt.

$ brew install opam
$ apt install opam

First-time setup: clones opam-repository and creates a default switch.

$ opam init

Put the active switch's binaries on PATH for this shell. opam init offers to add this to your shell profile; if you declined, run it yourself.

$ eval $(opam env)

Confirm which compiler you are actually running.

$ ocaml -version
The OCaml toplevel, version 5.2.0
§ 03

Switches

A switch is a directory with its own compiler and its own packages. A global switch lives under ~/.opam and is selected by name. A local switch lives in a _opam/ directory inside the project and activates automatically when you cd into it.

List the compiler versions available to install.

$ opam switch list-available

Create a named global switch on a specific compiler.

$ opam switch create 5.2.0

Create a local switch for the project in the current directory. The trailing dot is the directory, not the version.

$ opam switch create . 5.2.0
creates ./_opam

Create a switch with compiler variants enabled - flambda for aggressive inlining, or the sanitizers.

$ opam switch create 5.2.0+flambda ocaml-variants.5.2.0+options ocaml-option-flambda

See every switch, with an arrow on the active one.

$ opam switch list

Switch to another one, then re-evaluate the environment. The env step is not optional - without it your shell still points at the previous compiler.

$ opam switch set 5.1.1
$ eval $(opam env)

Run a single command under a different switch without changing the active one.

$ opam exec --switch=5.1.1 -- dune build

Delete a switch and everything installed into it.

$ opam switch remove 5.1.1
§ 04

Finding and installing packages

Refresh the package metadata. Do this before complaining that a newly released version is missing.

$ opam update

Search by name or description.

$ opam search json

Read a package's description, dependencies, and available versions.

$ opam show yojson

Install one or more packages into the active switch.

$ opam install yojson lwt

Install a specific version.

$ opam install yojson.2.1.2

List what is installed in the active switch.

$ opam list

List only the packages you asked for, rather than every transitive dependency.

$ opam list --installed-roots

Upgrade everything the solver can move forward.

$ opam upgrade

Upgrade a single package and whatever it drags along.

$ opam upgrade yojson

Remove a package.

$ opam remove yojson

Remove a package along with dependencies nothing else needs any more.

$ opam remove --auto-remove yojson
§ 05

Project dependencies

A project declares its dependencies in an .opam file at the repository root. With Dune you normally do not write that file by hand - you declare the package in dune-project and let dune build generate it.

In dune-project: declare the package and its dependencies, with version bounds where they matter.

$ (lang dune 3.16)
$ (generate_opam_files true)
$
$ (package
$ (name hello)
$ (synopsis "An example")
$ (depends
$ (ocaml (>= 4.14))
$ dune
$ yojson
$ (alcotest :with-test)))

Regenerate hello.opam after editing that stanza.

$ dune build

Install every dependency listed by the .opam files in this directory, including test-only ones.

$ opam install . --deps-only --with-test

The usual first two commands in a fresh clone: a local switch, then its dependencies.

$ opam switch create . 5.2.0 --no-install
$ opam install . --deps-only --with-test --with-doc

Ask why a package is installed - which of your roots pulled it in.

$ opam list --required-by yojson
$ opam list --depends-on yojson
§ 06

Pinning

Pinning replaces a package's published definition with one you point at: a local directory, a git URL, a branch. It is how you test a patch to a dependency before it is released, and how you consume a library that was never published at all.

Pin a dependency to a local checkout you are editing.

$ opam pin add yojson ../yojson

Pin to a git branch or tag.

$ opam pin add yojson git+https://github.com/ocaml-community/yojson.git#master

Record the pin but do not build it yet - useful when you are about to pin several things and want one solver run at the end.

$ opam pin add --no-action yojson ../yojson
$ opam install yojson

Re-read a local pin after you have changed its source. Pins are not watched.

$ opam upgrade yojson

See what is pinned, and undo it.

$ opam pin list
$ opam unpin yojson
§ 07

When the solver says no

A failed install usually means no version assignment satisfies every constraint at once. opam prints the conflict; these are the commands for working through it.

Show the plan without carrying it out.

$ opam install lwt --dry-run

Ask for a solution that leaves existing packages alone where possible, or the opposite - let it upgrade whatever it needs.

$ opam install lwt --criteria="-removed,-changed"
$ opam install lwt --unlock-base

Check whether the switch is currently consistent.

$ opam install --check
$ opam lint hello.opam

Reinstall a package from scratch when a build left something half-written.

$ opam reinstall yojson

Clear the download and build caches. This one is a real fix more often than it should be.

$ opam clean -a
§ 08

Reproducing a switch

Export the exact package set of the active switch.

$ opam switch export switch.export

Recreate that package set somewhere else - another machine, or CI.

$ opam switch import switch.export

Include the source of pinned packages in the export, so the import does not depend on those URLs still resolving.

$ opam switch export --full switch.export
§ 09

Publishing

Check the package file against the repository's rules before anyone else does.

$ opam lint hello.opam

Build a release tarball and the opam file that points at it.

$ dune-release tag
$ dune-release distrib

Open the pull request against opam-repository.

$ dune-release publish
$ dune-release opam pr

Or do the submission step alone, from an existing tag and archive URL.

$ opam publish https://github.com/you/hello/archive/v1.0.0.tar.gz
§ 10

Using a private or extra repository

Add a second repository to the active switch, at higher priority than the default.

$ opam repository add mine https://example.com/opam --rank=1

Add one globally, so every new switch picks it up.

$ opam repository add mine https://example.com/opam --all-switches

List repositories and their order.

$ opam repository list

Remove one.

$ opam repository remove mine

That is most of the loop: a switch per project, dependencies declared in dune-project and installed with --deps-only, pins while you are working against an unreleased change, and export/import when the same switch has to exist twice.