Opam 104: Sharing Your Code
Curious about the origins of opam?
Check out this short history on its evolution as the de facto package manager and environment manager for OCaml.
Welcome back to the opam deep-dives series!
In this article, we cover two essential topics for any OCaml developer:
- Setting up a development environment — how to quickly bootstrap a working environment for an existing OCaml project, including test dependencies, documentation tools, and IDE integration.
- Releasing your package — how to publish your OCaml library or tool to
the official
opam-repository, making it available to the entire community.
To get the most out of this article, you should be familiar with opam
fundamentals: what switches are, how to install packages, and how to write a
basic .opam file for your project.
If you're new to opam or need a refresher, check out our previous posts in
this series:
- Opam 101 — Getting started with
opam - Opam 102 — Pinning packages
- Opam 103 — Starting a new project and writing an
opamfile
Setting up a development environment
Last time, we got to the point where your project was set for a steady development process. Now, we look into how you can ease other developers into joining your development efforts.
Naturally, the first opam command a newcomer developer will use to
participate in your project is opam install. This is because their first goal
would be to reproduce the environment that you have set up for your project.
The default behaviour for a call to opam install is to install all necessary
dependencies for your project's binary to run. You can see this as opam
choosing to tailor the default usage to non-developer users.
However, of course, opam is a developer's best friend too and, for that
matter, provides a complete set of features, variables and command-line options
to help developers accomplish their goals. In the case of opam install, we
need to cover the following options, which all make a developer's life much
easier:
--deps-only, for minimal requirements;--with-test, for testing requirements;--with-doc, for building documentation;--with-dev-setup, for additional IDE tooling and extended QoL requirements.
opam install options and variables
Keep in mind that the following options can be used together, and that there
exist other opam features which utilise them too.
--deps-only
This option is crucial when setting up your development environment without
installing or rebuilding your project. It installs only the dependencies
listed in your *.opam file.
The recommended approach is to create a local switch for your project. This keeps your development environment isolated and project-specific:
$ opam switch create . --deps-only
This creates a new switch in the current directory and installs the minimal
set of dependencies needed to build the project. Local switches are stored in
a _opam/ directory within your project and are automatically selected when
you enter the project folder.
All the options described below (--with-test, --with-doc, --with-dev-setup)
can be combined with opam switch create:
$ opam switch create . --deps-only --with-test --with-doc --with-dev-setup
Note: If you already have a switch and want to install dependencies into it without creating a new one, use
opam install . --deps-onlyinstead.
Opam variables
The opam file can contain conditional dependencies, guarded by
variables. These variables (with-test, with-doc, with-dev-setup) allow
you to toggle sets of dependencies depending on your needs. For example,
test dependencies are only installed when the with-test variable is set.
💡 Interesting factoid, these variables can also be used in other fields such as
buildandinstall, check out the manual.
--with-test
$ opam install . --deps-only --with-test
This installs both main and test dependencies. It’s essential when preparing
an environment where you or others want to run or write tests. In your opam
file, such dependencies are usually declared with the {with-test} filter.
--with-doc
$ opam install . --deps-only --with-doc
Installs dependencies required to build documentation. Same as for
--with-test, such dependencies are declared with the {with-doc} filter
inside the depends: field. This is useful if you’re generating docs locally,
for instance with odoc through dune build @doc.
--with-dev-setup
$ opam install . --deps-only --with-dev-setup
This flag brings in development tooling dependencies, like linters,
formatters (e.g., ocamlformat), or editor integration (e.g., merlin).
These dependencies aren't required to build or test the project, but they
improve the quality-of-life for developers. In your opam file, they
would be tagged with {with-dev-setup}.
Quick setup: one command to rule them all
For a complete development environment dedicated to your project, use this single command:
$ opam switch create . --deps-only --with-test --with-doc --with-dev-setup
This command:
opam switch create .— creates a local switch in your project directory--deps-only— installs only dependencies, without building your project yet--with-test— includes test frameworks (e.g.,alcotest,ppx_expect)--with-doc— includes documentation tools (e.g.,odoc)--with-dev-setup— includes developer tooling (e.g.,merlin,ocamlformat)
You can omit any flag you don't need. For example, if you just want to run
tests without building docs, drop --with-doc.
At this stage, you should have a fresh switch ready—one to compile and run the
project you are onboarding to. However, there’s a catch: an .opam file might
not state which exact versions of dependencies are required by that
project. This can cause two developers on different machines to end up with
slightly different versions of the same packages — a common source of subtle
and hard-to-debug compatibility issues.
In practice, it’s common not to be overly specific about package versions in
the .opam file. The reason is that locking versions too tightly reduces the
range of compatible packages, which adds friction and makes life harder for
anyone who wants to use your package alongside other software (whether they are
developing it themselves or simply using it in another context). Instead, it’s
generally better to specify version ranges, which helps maintain flexibility
and avoids unnecessary restriction of the set of compatible packages within the
same switch.
depends: [
"ocaml" {>= "4.08"}
"dune" {>= "2.8"}
"menhir" {>= "2.1"}
"js_of_ocaml" {>= "3.9"}
"js_of_ocaml-ppx" {build & >= "3.9"}
"bisect_ppx" {with-test & >= "2.6" & dev}
"odoc" {with-doc}
]
Lock your dependencies with opam lock
There is a way however to make sure anybody who joins your project can quickly
setup a switch for themselves. Opam supports what is called opam.locked files.
You might be familiar with such configuration files in the form of
package-lock.json (for Javascript) or Pipfile.lock (for Python) which are
both much more verbose than an ordinary helloer.opam.locked file is.
To ensure everyone installs the exact same dependencies and version, you can use:
$ opam lock <package>
# Or use a local path to your project's directory
$ opam lock .
This generates a helloer.opam.locked file that freezes versions of all
dependencies as currently installed in your switch. It captures the exact
versions in use (notice the hard equality = in version constraints).
The process assumes your local switch is in a good state (build and tests succeed), then uses it to record the versions you have yourself used in practice.
# the local opam file
$ cat helloer.opam
opam-version: "2.0"
depends: [
"cmdliner"
"ocaml"
"alcotest" {with-test}
]
build: [
[ "dune" "build" "-p" name ]
[ "dune" "runtest" ] {with-test}
]
install: [ "dune" "install" ]
# Current content of the switch
$ opam list
# Packages matching: installed
# Name # Installed # Synopsis
alcotest 1.9.1 Alcotest is a lightweight and colourful test framework
astring 0.8.5 Alternative String module for OCaml
base-bigarray base
base-domains base
base-effects base
base-nnp base Naked pointers prohibited in the OCaml heap
base-threads base
base-unix base
camlp-streams 5.0.1 The Stream and Genlex libraries for use with Camlp4 and Camlp5
cmdliner 2.0.0 Declarative definition of command line interfaces for OCaml
cppo 1.8.0 Code preprocessor like cpp for OCaml
crunch 4.0.0 Convert a filesystem into a static OCaml module
dune 3.20.2 Fast, portable, and opinionated build system
fmt 0.11.0 OCaml Format pretty-printer combinators
fpath 0.7.3 File system paths for OCaml
heloer dev pinned to version dev at file:///home/developer/helloer/
ocaml 5.3.0 The OCaml compiler (virtual package)
ocaml-config 3 OCaml Switch Configuration
ocaml-syntax-shims 1.0.0 Backport new syntax to older OCaml versions
ocaml-system 5.3.0 The OCaml compiler (system version, from outside of opam)
ocamlbuild 0.16.1 OCamlbuild is a build system with builtin rules to easily build most OCaml projects
ocamlfind 1.9.8 A library manager for OCaml
odoc 3.1.0 OCaml Documentation Generator
odoc-parser 3.1.0 Parser for ocaml documentation comments
ptime 1.2.0 POSIX time for OCaml
re 1.14.0 RE is a regular expression library for OCaml
seq base Compatibility package for OCaml's standard iterator type starting from 4.07.
stdlib-shims 0.3.0 Backport some of the new stdlib features to older compiler
topkg 1.1.0 The transitory OCaml software packager
tyxml 4.6.0 A library for building correct HTML and SVG documents
uutf 1.0.4 Non-blocking streaming Unicode codec for OCaml
# Generating the lock file for 'helloer'
$ opam lock helloer
# the content of the opam lock file
$ cat helloer.opam.locked
opam-version: "2.0"
name: "helloer"
version: "dev"
depends: [
"alcotest" {= "1.9.1" & with-test}
"astring" {= "0.8.5" & with-test}
"base-bigarray" {= "base"}
"base-domains" {= "base"}
"base-effects" {= "base"}
"base-nnp" {= "base"}
"base-threads" {= "base"}
"base-unix" {= "base"}
"cmdliner" {= "2.0.0"}
"dune" {= "3.20.2" & with-test}
"fmt" {= "0.11.0" & with-test}
"ocaml" {= "5.3.0"}
"ocaml-config" {= "3"}
"ocaml-syntax-shims" {= "1.0.0" & with-test}
"ocaml-system" {= "5.3.0"}
"ocamlbuild" {= "0.16.1" & with-test}
"ocamlfind" {= "1.9.8" & with-test}
"re" {= "1.14.0" & with-test}
"stdlib-shims" {= "0.3.0" & with-test}
"topkg" {= "1.1.0" & with-test}
"uutf" {= "1.0.4" & with-test}
]
build: [
["dune" "build" "-p" name]
["dune" "runtest"] {with-test}
]
Developers who clone your repository and run:
$ opam install . --locked
will get the same versions of everything.
💡 The lock file is especially useful when working in teams or in CI. It increases reproducibility and reduces "But it works on my machine" issues.
By default, the lock file is named <package>.opam.locked. You can customize
the suffix with --lock-suffix, but then remember to pass the same suffix when
running opam install.
⚠️ Note: This guarantees a reproducible development environment, but only if you work on the same
opam-repositoryas your peers, not a bit-for-bit reproducible build, which is a broader topic involving build sandboxes and source hashes... Maybe a topic for another time.
Locking and pinning
Pins deserve special attention. If your project depends on pinned packages,
opam lock will record them as well. opam will use a specific field in the
opam file named pin-depends which allows you to list the packages (and their
respective URLs) that opam will automatically pin when installing the main
package.
pin-depends: [ "js_of_ocaml.dev" "git+https://github.com/ocsigen/js_of_ocaml#win-test" ]
If the pin is a local path, but a remote exists with a branch or hash, opam
will record the remote version.
$ opam pin
cmdliner.2.0.0 git git+file:///home/developer/cmdliner#master (at 0123456789c0ffee123456789abcdefedcba9876)
helloer.dev rsync file:///home/developer/helloer
$ opam lock .
[NOTE] Local pin git+file:///home/developer/cmdliner#master resolved to git+https://erratique.ch/repos/cmdliner#master
Generated lock files for:
- helloer.dev: /home/developer/helloer.opam.locked
$ cat helloer.opam.locked
[...]
pin-depends: [ "cmdliner.2.0.0" "git+https://erratique.ch/repos/cmdliner#master" ]
If no remote is available, the lock file may still include the local pin. If
you want to keep it that way, use --keep-local (available since 2.4). In
this case, you may need to edit the lock file before sharing it.
$ opam pin
cmdliner.2.0.0 git git+file:///home/developer/cmdliner#local-branch (at 0123456789deadc0dee0123456789abcdefedcba)
$ opam lock helloer
[WARNING] Referenced git branch for cmdliner.2.0.0 is not available in remote: git+https://erratique.ch/repos/cmdliner, use default branch instead.
[NOTE] Local pin git+file:///home/developer/cmdliner#local-branch resolved to git+https://erratique.ch/repos/cmdliner
Generated lock files for:
- helloer.dev: /home/developer/helloer.opam.locked
$ cat helloer.opam.locked
[...]
pin-depends: [ "cmdliner.2.0.0" "git+https://erratique.ch/repos/cmdliner" ]
$ opam lock . --keep-local
[NOTE] Dependency cmdliner.2.0.0 is pinned to local target git+file:///home/developer/cmdliner#local-branch, keeping it.
Generated lock files for:
- helloer.dev: /home/developer/helloer.opam.locked
$ cat helloer.opam.locked
[...]
pin-depends: [ "cmdliner.2.0.0" "git+file:///home/developer/cmdliner#local-branch" ]
Releasing your package
Once your project is stable and you’re ready to release it to the OCaml ecosystem, it’s time to publish it.
Publishing means submitting your package to an opam repository (most often
the official OCaml one). Once merged,
anyone can install your package with a simple opam install <your-package>.
Releasing with opam-publish
The easiest way to publish is via the opam publish plugin.
You can get the exhaustive list of opam plugins with the following call:
$ opam list --has-flag plugin
# Packages matching: has-flag(plugin) & (installed | available)
# Name # Installed # Synopsis
[...]
opam-publish -- A tool to ease contributions to opam repositories
[...]
Plugins are topics for another time. 😉
If you already have opam, you have nothing more to do than invoke that plugin
and it will automatically be fetched and installed within your current switch.
If you have installed opam publish in the past, opam will find it no matter
your current switch.
Workflow with GitHub
- Tag a release in your git repository, e.g., inside your
world-org/helloer, with version1.2.3. - Run:
$ opam publish world-org/helloer --tag 1.2.3
- The tool will:
- Validate your
opamfile (linting, formatting, metadata checks). - Since the GitHub tarball URL is automatically generated when you tag a new
release,
opam-publishuses that URL, generates a checksum for it and adds them both to theopamfile. - The tool will clone the
opam repository, commit your package, push a branch, and open a pull request automatically.
$ opam publish world-org/helloer --tag 1.2.3
The following will be published:
- helloer version 1.2.3 with opam file from the upstream archive
archive at https://github.com/world-org/helloer/archive/refs/tags/1.2.3.tar.gz
You will be shown the patch before submitting.
Please confirm the above data. Continue ? [Y/n] y
Please generate a Github token at https://github.com/settings/tokens/new to allow access.
The "public_repo" and "workflow" scopes are required ("repo" if submitting to a private opam repository).
Please enter your GitHub personal access token:
The token will be stored in ~/.opam/plugins/opam-publish/ocaml%opam-repository.token.
Fetching the package repository, this may take a while...
commit 0123456789abcdef0123456789abcdef01234567 (HEAD -> master)
Author: Welcomer <hell@er.com>
Date: Tue Oct 21 11:09:41 2025 +0200
1 package from world-org/helloer at 1.2.3
diff --git a/packages/helloer/helloer.1.2.3/opam b/packages/helloer/helloer.1.2.3/opam
new file mode 100644
index 0000000000..ea8f4010a0
--- /dev/null
+++ b/packages/helloer/helloer.1.2.3/opam
@@ -0,0 +1,48 @@
+opam-version: "2.0"
+synopsis: "HW"
+description: "A simple tool to display several types of 'hello world'"
+maintainer: ["Welcomer <hell@er.com>"]
+authors: ["Welcomer <hell@er.com>"]
+tags: ["toy project"]
+homepage: "https://github.com/OCamlPro/opam_bp_examples"
+doc: "https://url/to/documentation"
+bug-reports: "https://github.com/OCamlPro/opam_bp_examples"
+depends: [
+ "cmdliner"
+ "ocaml"
+ "alcotest" {with-test}
+]
+build: [
+ [ "dune" "build" "-p" name ]
+ [ "dune" "runtest" ] {with-test}
+ [ "dune" "build" "@doc" ] {with-doc}
+]
+install: [ "dune" "install" ]
+dev-repo: "https://github.com/OCamlPro/opam_bp_examples"
+url {
+ src: "https://github.com/world-org/helloer/archive/refs/tags/1.2.3.tar.gz"
+ checksum: [
+ "md5=0123456789abcdef0123456789abcdef"
+ "sha512=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+ ]
+}
No newline at end of file
File a pull-request for this patch ? [Y/n]
💡 About GitHub tokens: As shown above,
opam-publishrequires a GitHub personal access token to clone, commit, and open a PR on your behalf. You only need to enter it once—it's stored locally at~/.opam/plugins/opam-publish/ocaml%opam-repository.tokenuntil expiration. The tool prompts you with the necessary URL and scope (public_repoandworkflowfor public repositories,repofor private ones). Once you confirm the final prompt, the PR is automatically opened and your browser displays the PR page for you to track its progress.
Once the PR is reviewed and merged by maintainers, your package becomes available to everyone.
Without GitHub
If you don’t use GitHub releases, you can provide a URL to a tarball that contains the source and the project opam file :
$ opam publish https://world.com/helloer-1.0.0.tar.gz
opam-publish will handle the rest in a similar way.
💡 You may also provide
opam-publishwith a URL pointing to a different GitHub repository than the official OCaml one. Indeed, there exist other repositories in the wild, public or private, which you may want to publish to. You can use:opam publish --repo other-org/opam-repositoryfor that.
What about dune-release?
You may have heard of
dune-release, another popular tool
for releasing OCaml packages. While opam-publish focuses solely on submitting
your package to an opam repository, dune-release is a more comprehensive
release workflow tool that handles the entire process:
- Creating and pushing git tags
- Generating changelogs
- Creating GitHub releases with release notes
- Building and uploading distribution tarballs
- Submitting to opam-repository
If you're looking for an all-in-one release automation tool, dune-release is
worth exploring. However, if you prefer to manage your git tags and releases
manually and just need help with the opam submission step, opam-publish is
simpler and does exactly that.
Manual publishing (if you need it)
You can also publish manually, but it’s more work:
- Clone the
opam-repository:
$ git clone https://github.com/ocaml/opam-repository
- Create a new subdirectory:
$ mkdir -p packages/your-package/your-package.version/
- Add your
opamfile there (with aurlsection containing tarball + checksum). - Use
opam lintto verify that youropamfile is valid.
$ opam lint packages/your-package/your-package.version/opam
- Commit, push and open a pull request.
This works but requires you to take care of validation and formatting
yourself. That's why opam-publish is strongly recommended — it makes things
easier for the opam repository maintainers team.
Wrapping up the opam10x series
With these tools in hand:
opam install . --deps-only --with-doc --with-test --with-dev-setupto quickly jump into a project;opam lockto freeze versions of the packages of your current switch;opam publishto share your work with the world;
…you now have the full pipeline to build, collaborate, and distribute OCaml projects like a pro... An OCamlPro...
Thank you for tagging along this first salvo of opam deep-dives.
From Opam 101 in January 2024 to
Opam 104 today, we have covered what we believe is key to stepping into the OCaml
ecosystem organically. With these articles you have a first-hand demonstration
of the tooling, philosophy and workflow, and you should be able to get started
on the path to becoming a fully-fledged OCaml contributor!
Keep building, keep sharing, and keep the OCaml ecosystem thriving! 🐫💚
Thank you for reading,
From 2011, with love,
The OCamlPro Team.
This article series was made possible thanks to the support of the OCaml Software Foundation. Thank you for helping us share knowledge with the OCaml community!
About OCamlPro:
OCamlPro is a R&D lab founded in 2011, with the mission to help industrial users benefit from experts with a state-of-the-art knowledge of programming languages theory and practice.
- We provide audit, support, custom developer tools and training for both the most modern languages, such as Rust, Wasm and OCaml, and for legacy languages, such as COBOL or even home-made domain-specific languages;
- We design, create and implement software with great added-value for our clients. High complexity is not a problem for our PhD-level experts. For example, we helped the French Income Tax Administration re-adapt and improve their internally kept M language, we designed a DSL to model and express revenue streams in the Cinema Industry, codename Niagara, and we also developed the prototype of the Tezos proof-of-stake blockchain from 2014 to 2018.
- We have a long history of creating open-source projects, such as the Opam package manager, the LearnOCaml web platform, and contributing to other ones, such as the Flambda optimizing compiler, or the GnuCOBOL compiler.
- We are also experts of Formal Methods, developing tools such as our SMT Solver Alt-Ergo (check our Alt-Ergo Users' Club) and using them to prove safety or security properties of programs.
Please reach out, we'll be delighted to discuss your challenges: contact@ocamlpro.com or book a quick discussion.
Most Recent Articles
2025
2024
- opam 2.3.0 release!
- Optimisation de Geneweb, 1er logiciel français de Généalogie depuis près de 30 ans
- Alt-Ergo 2.6 is Out!
- Flambda2 Ep. 3: Speculative Inlining
- opam 2.2.0 release!
- Flambda2 Ep. 2: Loopifying Tail-Recursive Functions
- Fixing and Optimizing the GnuCOBOL Preprocessor
- OCaml Backtraces on Uncaught Exceptions
- Opam 102: Pinning Packages
- Flambda2 Ep. 1: Foundational Design Decisions
- Behind the Scenes of the OCaml Optimising Compiler Flambda2: Introduction and Roadmap
- Lean 4: When Sound Programs become a Choice
- Opam 101: The First Steps
2023