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.
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.
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 -versionThe OCaml toplevel, version 5.2.0
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.0creates ./_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
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
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
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
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
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
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
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.