Hello Guix Users!

In the past I thought I had it figured out, thanks to help from someone on the mailing list, how to run Ocaml projects in a reproducible (hashes/checksums verified!) way.

It is December and I thought I would give Ocaml a try at Advent of Code this year. However, I learned, that my previous project structure/setup no longer works. I thought: "Why not check out how people, who don't use or know guix do this?" and looked into opam. Long story short: Unfortunately, it does not provide a lock file that one could commit into a repo to ensure reproducibility. Even the tool opam-lock merely pins version numbers without hashes. Disappointing. So I am back to trying to fix my guix using project setup. Last stop before giving up on Ocaml for another year or so. In the following I will describe my project setup, and why it hopefully is reproducible not only on my machine, but also on other machines.

*The guix part*

I have the following files:

(1) guix-env/channels.scm: Specifying the exact guix channel that is used, and thereby locking down hashes, which are specified in the guix repository itself.

~~~~
(list (channel
        (name 'guix)
        (url"https://git.guix.gnu.org/guix.git";)
        (branch "master")
        (commit
          "7c6d8a6224cf3209efa179dbe1509759a580cb05")
        (introduction
          (make-channel-introduction
            "9edb3f66fd807b096b48283debdcddccfea34bad"
            (openpgp-fingerprint
              "BBB0 2DDF 2CEA F6A8 0D1D  E643 A2A0 6DF2 A33A 54FA")))))
~~~~

(2) guix-env/manifest.scm: Specifying the package, that I want to use for my project.

~~~~
(specifications->manifest
 '("[email protected]"
   "ocaml-utop"
   "dune"
   "bash"
   "make"
   "ocaml-findlib"
   "ocaml-zarith"))
~~~~

(3) Makefile: Used as a task runner, basically, to avoid having to type complicated commands.

~~~~ Makefile (variable definitions) ~~~~
.POSIX:
.RECIPEPREFIX = >
.DELETE_ON_ERROR:

MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules

SHELL ::= guix time-machine --channels=guix-env/channels.scm -- shell --check 
--manifest=guix-env/manifest.scm -- bash -c
.SHELLFLAGS ::= -Euxo pipefail -c

BUILD_DIR ::= _build

OCAML_PACKAGES ::= zarith

...
~~~~

Here one can see, that I am setting the `SHELL` environment variable to using `bash -c`, which will evaluate the actual commands that are defined later in Makefile targets. `bash` runs in the `guix shell`, that is created in a `guix time-machine` call, which refers to the `guix-env/channels.scm` file, to make sure, that I am using exactly the versions defined in that commit of the guix repository, implying that I am using exactly the hashes defined on that commit of the guix repository. That should make things reproducible and make it so that when I run things on another machine, the same software, the exact same packages are used.

I also define a list of Ocaml packages I am using, currently only one: `zarith` which is important for big integers.

Then come the actual targets:

~~~~ Makefile (targets) ~~~~
...

.PHONY: repl
repl:
utop


.PHONY: shell
shell:
bash

...
~~~~

The target `shell` is merely for debugging purposes, or for when I need to run a command in a shell that has the dependencies installed.

When I run `make shell` and in that resulting shell run `ocaml --version`, I get the output: `The OCaml toplevel, version 5.3.0`. Great so far!

*(Issue 1)*

One problem is with `utop`. Somehow it does not use the correct Ocaml version and I don't know where it gets another Ocaml version even from. When I run `make repl` I get a `utop` REPL that says:

~~~~ utop on command line ~~~~
───────────┬───────────────────────────────────────────────────────────────────┬
           │ Welcome to utop version %%VERSION%% (using OCaml version 4.14.1)! │
           └───────────────────────────────────────────────────────────────────┘

Type #utop_help for help about using utop.
~~~~

What? Why is it using Ocaml version 4.14.1?? Where does it get that from? No idea. It shouldn't be using anything but the installed packages. Maybe it internally depends on an older Ocaml version implicitly, and guix installs that, so that utop can run at all, satisfying utop's dependencies? But on https://hpc.guix.info/package/ocaml-utop I click the link to https://codeberg.org/guix/guix/src/commit/23dbcfaef682c41e9cb2a6aea9ad69feea65f26a/gnu/packages/ocaml.scm#L5660, where I see the inputs:

~~~~ ocaml.scm ~~~~
    (native-inputs
     (list ocaml-cppo))
    (propagated-inputs
     (list ocaml-lambda-term
           ocaml-logs
           ocaml-lwt
           ocaml-lwt-react
           ocaml-react
           ocaml-zed))
~~~~

There I at least don't see any direct dependency on an older Ocaml version. Might be one of those packages listed there is only available for older Ocaml version. I don't know.

But this issue is not the main issue. The main issue is with other targets. The Makefile also has targets to run an Ocaml file:

~~~~ Makefile (targets for running ocaml things) ~~~~
%.byte: %.ml
ocamlfind ocamlc $(foreach OCAML_PACKAGE,$(OCAML_PACKAGES),-package 
$(OCAML_PACKAGE)) -linkpkg -o $*.byte $<


# Simply running depends on building the byte files, which
# are to be considered intermediate files.
.PRECIOUS: %.byte
%: %.byte
./[email protected]
~~~~

Here I define a target `%`. This target will be used when I write things like `make main`. It depends on `%.byte`, which means that `main` depends on `main.byte`. The other target `%.byte` describes how to build that. It depends on there being a `%.ml` which in case of `main` would be `main.ml`. It runs the long command written there, which makes use of `ocamlfind`. The hint to use `ocamlfind` is the decisive hint someone gave me on this mailing list some time ago. If there is a better way, please let me know. I need to be able to specify arbitrary packages, that I install using guix, by writing them in the `guix-env/manifest.scm` file.

*(Issue 2)*

So lets see what actually happens, when I try to run some `main.ml` file with some code:

~~~~ main.ml ~~~~
(* In REPL instead use:
   #require "zarith";; *)

open Z
~~~~

`make main`:

~~~~ make main result ~~~~
ocamlfind ocamlc -package zarith -linkpkg -o main.byte main.ml
guix shell: checking the environment variables visible from shell '/bin/bash'...
guix shell: All is good!  The shell gets correct environment variables.
+ ocamlfind ocamlc -package zarith -linkpkg -o main.byte main.ml
findlib: [WARNING] Package unix has multiple definitions in 
/gnu/store/q1jdsy1y8kzk18iy4snb9c85m6awl2jp-profile/lib/ocaml/unix/META, 
/gnu/store/q1jdsy1y8kzk18iy4snb9c85m6awl2jp-profile/lib/ocaml/site-lib/unix/META,
 
/gnu/store/yjik976n23235nhkr0amkrymb6kyfkxs-ocaml-findlib-1.9.5/lib/ocaml/site-lib/unix/META
findlib: [WARNING] Package threads has multiple definitions in 
/gnu/store/q1jdsy1y8kzk18iy4snb9c85m6awl2jp-profile/lib/ocaml/threads/META, 
/gnu/store/q1jdsy1y8kzk18iy4snb9c85m6awl2jp-profile/lib/ocaml/site-lib/threads/META,
 
/gnu/store/yjik976n23235nhkr0amkrymb6kyfkxs-ocaml-findlib-1.9.5/lib/ocaml/site-lib/threads/META
File "main.ml", line 1:
Error: 
/gnu/store/q1jdsy1y8kzk18iy4snb9c85m6awl2jp-profile/lib/ocaml/site-lib/zarith/z.cmi
       is not a compiled interface for this version of OCaml.
It seems to be for an older version of OCaml.
make: *** [Makefile:23: main.byte] Error 2
~~~~

For starters, the `findlib` warnings seem to be worrisome. Why does findlib ever search somewhere, where it finds multiple definitions? I don't remember this issue from this project setup appearing 1 or 2 years ago. This might already be the cause for the next part of the issue.

~~~~
Error: 
/gnu/store/q1jdsy1y8kzk18iy4snb9c85m6awl2jp-profile/lib/ocaml/site-lib/zarith/z.cmi
       is not a compiled interface for this version of OCaml.
It seems to be for an older version of OCaml.
~~~~

Well, seems like it is not using the correct package, or Ocaml version specified in manifest.scm, or the package got compiled using another Ocaml version, or I don't know what's going on and what is going wrong.

The issue with utop showing another version is already suspect. Maybe somehow the `zarith` package got compiled using that older version of Ocaml too!

But I don't know how to check that. Or how to stop Guix from doing that. Or how to nail down the versions in a way that doesn't allow this to happen.

I already tried running `guix gc` to maybe delete old `guix shell`s, but to no avail.

Here is a repository, which contains the project setup I described: https://codeberg.org/ZelphirKaltstahl/advent-of-code-2025/src/commit/aa7d26056fc0ff442978d4bdebe615d533e54db4.

Here is my `guix --version` output, in case it is relevant:

~~~~ guix --version output ~~~~
guix (GNU Guix) 7c6d8a6224cf3209efa179dbe1509759a580cb05
Copyright (C) 2025 the Guix authors
License GPLv3+: GNU GPL version 3 or later<http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
~~~~

Here is my OS:

~~~~ lsb_release -a ~~~~
No LSB modules are available.
Distributor ID: Debian
Description:    Debian GNU/Linux 12 (bookworm)
Release:        12
Codename:       bookworm
~~~~

So my questions are at this point:

(1) How do people make reproducible Ocaml project setups? I don't mean only version numbers ... It must be hashes and verifying those hashes. Otherwise I don't think I will pursue things further with Ocaml.

(2) How to fix my guix-using Ocaml project setup, so that I can run my code, including ocaml-* packages that I install via guix?

I am aware, that I could also create a guix profile. However, I don't think that will change things and I am fine with a guix shell. All I want is something reproducible, whether that is a guix profile or a guix shell, doesn't really matter to me. guix shells are also cached, so it is not like I need to rebuild the guix shell at every call of my Makefile targets either.

I am at the end of my wits. If anyone can point me to a reproducible project setup, or tell me what I am doing wrong in my setup, that would be great : )

Best regards,
Zelphir

--
repositories:https://codeberg.org/ZelphirKaltstahl

Reply via email to