How can my code know my program's version?

In an example in the Cmdliner tutorial tutorial (cmdliner.tutorial) I see the promising line

  Cmd.make (Cmd.info "TODO" ~version:"%%VERSION%%") @@
  ...

so I guess there should be some kind of preprocessing to replace that %%VERSION%% string with the correct value, taken from a “single source of truth”, but I cannot find either. What/where should be that single source of truth, and what should be that expansion mechanism?

One candidate for the source of truth is probably the version stanza in dune-project?

As for the mechanism, I saw something a bit different in some online projects, with dune files with

(rule
 (targets version.ml)
 (action
  (with-stdout-to %{targets}
   (echo "let version = \"%{version:my_project}%\""))))

which would allow for instance to write in my code

  Cmd.make (Cmd.info "TODO" ~version:Version.version) @@
  ...

But unfortunately, that does not display the version in the version stanza, but another string that I cannot make sense of, like 2b8df27-dirty%.

Is there a “canonical” or at least a preferred way to obtain a version number in an OCaml project?

Thank you.

That looks like the result of git describe --dirty --always. This means you are at commit 2b8df27 and your checkout is dirty.

The day you git tag -a a commit, the command will output the tag and thus the version if your tag is a version.

See How to make dune include version number in a library module - #3 by Juloo

I don’t like patching of binary files and use something simpler:

#! /bin/sh
# build.sh
cat <<EOF
let git_revision = "$(git describe --tags --always --dirty)"
let build_time = "$(date)"
EOF

(rule
 (target build.ml)
 (deps (universe) build.sh)
 (action
  (with-stdout-to
   %{target}
   (bash "./build.sh"))))

This creates a file build.ml which gets compiled. Now I can use:

let main_cmd =
  let version = Build.git_revision in
  ...
C.Cmd.eval_result main_cmd

This only works on macOS and Linux.

Fairly sure you can get it to work on any OCaml supported OS by replacing the shell script with an OCaml file that dune will compile and run for you.

I think that’s what the OP did (in pure dune) !

Thank you, I understand a bit better the mechanism. It seems that there are mainly two methods: the dune subst command and a rule stanza that generates a .ml file with the data of \"%{version:my_project}%\".

It seems that both end up using a version (or tag) set by git (and dune subst looks to be usable only when one releases a package). However, there is another solution to the question linked by @yawaramin, which proposes to use dune-buid-info (new links: Dune Libraries - Dune documentation and dune-build-info 3.22.0 · OCaml Package). It uses the version indicated in the version stanza of dune-project and looks like the preferred way.

I recommend to not embed the build time - it in my opinion doesn’t include any useful information (which the git describe lacks); and it destroys reproducibility (see https://reproducible-builds.org/) – also see do not embed version and timestamp information by hannesm · Pull Request #90 · backtracking/ocamlgraph · GitHub as an example).

In respect to how to do this with dune, I am aware of patch/bin/dune at main · hannesm/patch · GitHub (which I don’t know how it works, or how dune determines the version, but it seems to work nicely).

Reproducibility depends on the entire state of Opam, which is not captured by Git if we only look at the code of the application that we are building. In any case, this is just a demonstration how to easily include information from the environment at build time.

I never claimed that “not including the date will provide you with reproducibility”.

I codeveloped several years ago tooling to capture what influences reproducibility, take a look at orb GitHub - robur-coop/orb: check opam package reproductibility · GitHub if you’re interested. It includes environment variables, the opam swtich export --full --freeze, and system packages.

All I intended to say is that a timestamp doesn’t really give you any further information: it neither tells you which opam-repository commit you were at, or which OS / last package update; but actively harms reproducibility (see the previously linked PR to ocamlgraph) – thus I suggest to not include it, not even in “demonstration how to easily include information from the environment”.