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!
We pick up today's topic exactly where we left it last time.
Opam 103 brought us halfway through the lifecycle of an OCaml project. We
learned about the opam environment, its characteristics, how to lookup OCaml
packages, integrate external libraries, build your binary, run your tests and
prepare your project for distribution by writing an opam file.
Today we cover the other half of this lifecycle. We are going to look at how
you can use opam to ease other developers into your project, and later
distribute your package to all opam users!
Features that make teamwork flow
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.
For example:
$ opam install . --deps-only
This installs the minimal set of dependencies needed to build the project. It’s the quickest way to install dependencies inside an existing switch for development without touching the project itself.
Note: You could also choose to bootstrap a specific local switch for that project with
opam switch . --deps-only.
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}.
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 package.opam.locked file is.
To ensure everyone installs the exact same dependencies, you can use:
$ opam lock <package>
# Or use a local path to your project's directory
$ opam lock .
This generates a package.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 numbers).
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.
$ cat package.opam
opam-version: "2.0"
depends: [
"cmdliner"
"ocaml"
"alcotest" {with-test}
]
build: [
[ "dune" "build" "-p" name ]
[ "dune" "runtest" ] {with-test}
]
install: [ "dune" "install" ]
$ 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
package dev pinned to version dev at file:///home/developer/project/
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
$ opam lock package
$ cat package.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/project
$ 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/package.opam.locked
$ cat package.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 package
[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:
- package.dev: /home/developer/package.opam.locked
$ cat package.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:
- package.dev: /home/developer/package.opam.locked
$ cat package.opam.locked
[...]
pin-depends: [ "cmdliner.2.0.0" "git+file:///home/developer/cmdliner#local-branch" ]
Sharing your OCaml package with the community
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
[...]
We will talk more about plugins in a later article. 😉
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
my-org/ocaml-project, with versionv1.0.0. - Run:
$ opam publish my-org/ocaml-project --tag v1.0.0
- 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 PR automatically.
$ opam publish my-org/my-project --tag 1.2.3
The following will be published:
- my-project version 1.2.3 with opam file from the upstream archive
archive at https://github.com/my-org/my-project/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" scope is 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 my-org/my-project at 1.2.3
diff --git a/packages/opam-client/opam-client.2.5.0~alpha1/opam b/packages/opam-client/opam-client.2.5.0~alpha1/opam
new file mode 100644
index 0000000000..ea8f4010a0
--- /dev/null
+++ b/packages/opam-client/opam-client.2.5.0~alpha1/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/my-org/my-project/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_repofor 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:
$ opam publish https://my-org.com/ocaml-project-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 my-org/opam-repositoryfor 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 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.
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 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.
Rest assured, we have a lot more articles on the way which will cover more
complex topics with time and we hope that you will stay with us on the way to
being a fully-fledged OCaml contributor!
Until next time, keep building, keep sharing, and keep the OCaml ecosystem thriving! 🐫💚
Thank you for reading,
From 2011, with love,
The OCamlPro Team.
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