Table of contents

Opam 104: Sharing Your Code




alt text is alternative

alt text is alternative

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 build and install, 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-repository as 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
  1. Tag a release in your git repository, e.g., inside your my-org/ocaml-project, with version v1.0.0.
  2. Run:
$ opam publish my-org/ocaml-project --tag v1.0.0
  1. The tool will:
  • Validate your opam file (linting, formatting, metadata checks).
  • Since the GitHub tarball URL is automatically generated when you tag a new release, opam-publish uses that URL, generates a checksum for it and adds them both to the opam file.
  • 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-publish requires 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.token until expiration. The tool prompts you with the necessary URL and scope (public_repo for public repositories, repo for 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-publish with 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-repository for that.

Manual publishing (if you need it)

You can also publish manually, but it’s more work:

  1. Clone the opam-repository:
$ git clone https://github.com/ocaml/opam-repository
  1. Create a new subdirectory:
$ mkdir -p packages/your-package/your-package.version/
  1. Add your opam file there (with a url section containing tarball + checksum).
  2. Use opam lint to verify that your opam file is valid.
$ opam lint packages/your-package/your-package.version/opam
  1. 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-setup to quickly jump into a project;
  • opam lock to freeze versions of the packages of your current switch;
  • opam publish to 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.