opam-minver

opam-minver finds the minimum dependency versions for an opam project by directly testing them. Its goal is to give package authors accurate, tested lower bounds that maximize compatibility across OCaml compiler versions, so that users don't need to keep up with the latest compiler to install packages. It can also help find tighter or more relaxed bounds on existing packages where the compatibility was never precisely tested.

Given a project directory containing a .opam file, it reads the declared dependencies, binary-searches each one's available versions to find the oldest version that still builds and passes tests, and optionally writes the discovered lower bounds back into the .opam file.

Installation

opam install opam-minver

Usage

Run from the project directory you want to analyze:

opam-minver

By default the tool runs in dry-run mode: it prints what it would write without modifying anything. Pass -w or --write to update the .opam file in place:

opam-minver --write

If the project is in a different directory, use --dir:

opam-minver --write --dir /path/to/my-package

Any existing dependency version bounds in the opam file will be used as limits to the search space. This can save time if you know of any incompatibilities, but it also means that if you want to search the full range regardless of what is already in the file, you should either remove those bounds manually or pass --ignore-bounds.

Subcommands and options

opam-minver [OPTION]…
opam-minver delete [--dry-run]

Option

Description

--write, -w

Write discovered bounds to the .opam file (default: dry-run)

--dir DIR

Project directory to analyze (default: current directory)

--opam-file FILE

Path to a specific .opam file; use when the project contains multiple .opam files

--compiler VERSION

Use this OCaml version as the compiler ceiling instead of the current switch compiler (see below)

--ignore-bounds

Search the full version range even when bounds are already present in the .opam file (see below)

--keep-switches

Keep temporary opam switches after the run

--keep-json

Keep opam-minver.json after a successful --write run

--log-file [FILE]

Enable debug logging. Generates a timestamped filename if FILE is omitted

--quiet, -q

Suppress per-probe progress output

The delete subcommand removes all opam-minver- switches without running a search. --dry-run shows what would be removed without actually deleting.

How it works

  1. Parse the .opam file to collect all declared dependencies.
  2. Probe the OCaml compiler first. The current compiler (or the version given by --compiler) is verified to build and test the project, then the available OCaml versions are binary-searched for the oldest passing version. OCaml 4 and OCaml 5 are searched independently, since a package can have different minimum requirements for each major version. If an OCaml lower bound already exists in the .opam file, the bound version is probed first within each series, and if it passes, the binary search for that series is skipped entirely.
  3. Probe each dependency within a dedicated switch for each OCaml major version. The switch is built with the minimum passing OCaml version found in the previous step, so the discovered dep bounds reflect what works at the oldest supported compiler. All dependencies are tested in the same switch (with pins applied one at a time) rather than creating a new switch per version to save time. As with the compiler versions, if a dependency already carries a lower bound in the .opam file, that bound version is probed first, and if it passes, the binary search is skipped entirely, so runs over a well-bounded file are significantly faster.
  4. Run combined validations. Once all per-dependency searches are finished, the discovered minimums are pinned together in a fresh switch and the project is built and tested again. This catches any interactions between packages that the independent per-dep searches could not see. If the minimum version for any dependency differs between OCaml 4 and OCaml 5, an additional validation installs the lower bounds without pinning on an OCaml 5 switch and builds and tests the project, proving that the written bounds are satisfiable on OCaml 5.
  5. Write the results back into the .opam file's depends: block once all searches are finished. Each dependency gets a single >= version constraint. If the minimum version differs between OCaml 4 and OCaml 5, the lower (OCaml 4) bound is written: opam's solver will automatically select a higher compatible version on OCaml 5 when needed. A note is printed listing the packages where bounds differ. Switches are removed unless --keep-switches was passed.

The binary search assumes for each dependency that if version N passes, all later versions also pass. This holds for the vast majority of packages. Interdependencies between packages, where the minimum version of one dep depends on the version of another, are not modeled: each dep is searched independently. This keeps the search space small.

Resumability

Progress is saved to opam-minver.json in the project directory after every probe. If a run is interrupted, restarting opam-minver in the same directory will pick up where it left off. Already-known pass/fail results are returned immediately from the cache without touching opam, and existing switches are reused rather than recreated.

The final computed bounds from the last fully-completed run are also saved as a "confirmed" snapshot. If you run with --write and every dependency currently declared in the .opam file is covered by that snapshot, the bounds are written directly with no searching at all, so a dry run to see what would be found, followed by --write to commit it, doesn't need to repeat the exact same flags to avoid re-probing. This allows you to do a search with, for instance, a specified compiler, and then later run --write alone to commit those bounds without repeating the compiler flag. This is skipped (falling back to a normal search) in the one case where it would be unsafe: --ignore-bounds is passed on the current run but the snapshot was recorded without it, since that snapshot's search was narrowed to the bounds already in the .opam file and may not reflect the wider range --ignore-bounds is asking for. A snapshot recorded with --ignore-bounds is always reusable, since it already covers the full range.

OCaml 4 and OCaml 5 compatibility

When a dependency needs a different minimum version on OCaml 5 than on OCaml 4, opam-minver writes the lower bound. The opam solver will handle this correctly: for example, a user on OCaml 5 who installs a package with >= "1.0" will get the oldest version compatible with their system, which may be 1.2, without any extra work on the package author's part.

Limitations

generate_opam_files projects. If the dune-project file contains (generate_opam_files true), the .opam file is managed by dune and would be overwritten on the next build. opam-minver detects this and prints the discovered bounds without writing them, even if --write is passed.

Multiple .opam files. If a project directory contains more than one .opam file, opam-minver will halt with an error unless you specify which file to use with --opam-file.

Stale cache. If you change the project's compatibility requirements: for example, dropping OCaml 4 support, raising a lower bound manually, or adding a new dependency, delete opam-minver.json before re-running. Without this, the cached results from the previous run may prevent the new bounds from being found correctly. Adding or removing a dependency invalidates the confirmed-bounds snapshot automatically (see "Resumability" above), but changes to an existing dependency's constraints do not, so deleting the cache remains the reliable way to force a fully fresh search.

One instance at a time. Because this program uses opam switches, which are global, it cannot be run in parallel with other instances. It doesn't check for this: simply do not use two instances in parallel.

The project must build. opam-minver first verifies that the project builds and tests pass with the currently active compiler (or the version given by --compiler) by running dune build and then dune test. If either of these steps fail, the run aborts. Switch to a compiler with which the project builds before running the tool.

--compiler VERSION is useful when the current switch is older than the compiler version you want to certify against. For example, if you are running OCaml 5.3 but want to discover minimum dependency bounds valid up to OCaml 5.5, pass --compiler 5.5.0. The specified version is verified first and sets the ceiling for the OCaml version search. Dependency bounds are still discovered using the minimum passing OCaml version found in that search, so bounds remain as low as possible regardless of the ceiling you specify.

--ignore-bounds is useful when a project already has dependency bounds and you want to check whether the bounds can be relaxed further: for example, after a previously-incompatible old version of a dependency has been updated, or if the bounds were never precisely determined to begin with. Without this flag, an existing >= "X" constraint is used both to narrow the search space and as a shortcut (the bound version is probed first: if it passes, the binary search is skipped). With --ignore-bounds, the tool ignores all existing lower bounds and probes the full available version range for every dependency. The upper bounds are still respected. To ensure opam's solver does not reject pinned versions that fall below the stated constraints, a temporary copy of the .opam file with lower bounds stripped is used for the install step.

License

MIT