Copy source code into a directory, add a vendor stanza, run dune build.
my-project/
src/
vendor/
fmt.0.9.0/ # copied from somewhere
yojson.2.0.0/ # copied from somewhere
dune # (vendor ...) stanzas
dune-project
Dune doesn't care where the source came from. It builds what's in the directory.
(vendor <directory>
(package <name>) ; optional: inferred from opam file
(version <version>) ; optional: inferred from opam file
(libraries <lib-spec>...) ; optional: inferred by scanning directory
(mode <build-mode>) ; optional: dune | opam (inferred from contents)
(install <bool>) ; optional: default true
(toolchain <name>)) ; optional: marks as compiler/toolchainAll fields are optional. Explicit values override inference.
Basic (equivalent to vendored_dirs):
(vendor vendor/fmt.0.9.0)Selective libraries:
(vendor vendor/fmt.0.9.0
(libraries fmt fmt.tty))Multi-version with aliasing:
(vendor vendor/yojson.1.7.0
(libraries (yojson :as yojson_v1)))
(vendor vendor/yojson.2.0.0
(libraries yojson))Non-dune package:
(vendor vendor/zarith.1.14)Dune detects the opam file and uses (mode opam) automatically.
Toolchain:
(vendor vendor/ocaml.5.2.0
(toolchain native))The <lib-spec> uses ordered set language:
:standard— all libraries in directory<name>— expose with original name(<name> :as <alias>)— expose under different name:standard \ <name>— all except specified
| Directory Contents | Default Mode |
|---|---|
Contains dune-project or dune files |
dune |
Contains only *.opam file |
opam |
In dune mode, the directory builds like normal dune code with library filtering
and aliasing applied.
In opam mode, dune runs the package's opam build commands.
See RFC: Opam Build Mode for details.
(install true)(default): artifacts go to_build/install/<context>/(install false): artifacts stay in vendor build directory only- Using
:asaliasing implies(install false)
Only one package with a given name can be installed per context. If two vendored
packages have the same name and both have (install true), dune errors with a
message suggesting (install false) on one.
Dune resolves at the library level. When code needs library "foo":
- Vendored dune packages in workspace
- Vendored opam packages (installed to shared prefix)
- Locked packages from dune.lock
- System OCAMLPATH
Dune sets OCAMLPATH so build commands discover dependencies via ocamlfind.
Dune does not require a closure of opam packages. If a vendored opam package depends on "foo" but the library is available from a dune package, that works.
No solver, no version constraints. Dependencies must be in the workspace or system.
When building an opam-mode package:
- Parse
depends:field, evaluate filters ({os = "linux"}, etc.) - For each dependency, look for it in workspace. If found, build it first. If not found, assume system provides it.
- Build with OCAMLPATH = workspace libraries + system
- If ocamlfind can't find a library: "Library X not found (required by package Y)"
(toolchain <name>) marks the package as providing a compiler:
(toolchain native)— native OCaml compiler(toolchain windows)— cross-compiler for(targets native windows)
Dune sets OCAMLFIND_TOOLCHAIN=<name> and builds toolchain packages first.
| Condition | Error |
|---|---|
(libraries foo) but foo not in directory |
library resolution error |
| Two vendors expose same library name | library resolution error |
(mode dune) but no dune files |
library not found |
(mode opam) but no opam file |
clear error message |
| Alias conflicts with existing library | library resolution error |
Fetches all locked packages to _build/.pkgs/<context>/<name>/source/. Sources are
transient and not version controlled. This is the default behavior during builds.
dune pkg fetch # Fetch all packages to _buildCopies package sources to duniverse/ for version control and editing.
dune pkg vendor # Vendor all packages
dune pkg vendor fmt zarith # Vendor specific packages
dune pkg vendor --all # Same as no argumentsAfter vendoring, packages build from duniverse/ instead of _build/.pkgs/.
The duniverse/dune file is auto-generated with appropriate vendor stanzas.
Options:
--force— Re-vendor even if already present (overwrites local changes)--output <dir>— Vendor to different directory (default:duniverse)
(vendored_dirs) continues to work. In vendor/dune, these are equivalent:
(vendored_dirs foo.1.0.0)
(vendor foo.1.0.0)Note: vendored_dirs only matches directories at the current level (no paths with /).
The %{bin-available:...} pform only detects:
- Local project binaries (from install stanzas and public executables)
- Binaries on PATH
- Binaries from
(vendor ... (mode dune))packages
It does not detect binaries from (vendor ... (mode opam)) packages. This is
because %{bin-available:...} is expanded during dune file loading (rule generation),
before opam-mode packages are built. Checking opam-mode binaries would require
resolving all packages, which creates dependency cycles.
Workaround: If you need to conditionally enable features based on binaries from opam-mode packages, use a different approach such as:
- Moving the binary check to build time (in a rule action)
- Using
(mode dune)for the package if possible - Assuming the binary will be available and handling errors at build time
Shared prefix vs isolated derivations?
Shared prefix (current approach):
- All packages install to
_build/install/<context>/ - Simple PATH/OCAMLPATH setup
- One version per package per context
Isolated derivations (nix-style):
- Each package in
_build/.pkgs/<context>/<name>-<build-id>/ - Multiple versions coexist naturally
- More complex PATH/OCAMLPATH setup