Home
Running the Rocq and Lean Provers in Testcontainers
Curiously neither Rocq (formerly Coq) nor Lean has any official write-up (that I could find) on how to invoke these provers from within a container. I’m using Docker and Java’s Testcontainers here, but most of this should generalize to other contexts.
Rocq
The Dockerfile
is simple enough that you can provide the image coordinates to Testcontainers directly instead, but I chose to create one for future customization:
FROM rocq/rocq-prover:9.0@sha256:853108e7c0bf2a04e2ecd970e0b22dc97d5ebbc99f7252942972f3eb670bcbf2
Assuming this Dockerfile
is placed in src/{main,test,testFixtures}/resources
, creation of the container via Testcontainers looks like this:
GenericContainer<?> container = new GenericContainer<>(
new ImageFromDockerfile()
.withFileFromClasspath("Dockerfile", "com/yourpackage/Dockerfile"))
.withCommand("sleep", "infinity")
.waitingFor(Wait.forSuccessfulCommand("eval $(opam env) && rocq -v"));
The rocq
binary is managed by opam, hence the need to bring in opam-defined environment variables. There’s probably a way to push the eval $(opam env)
into the Dockerfile
(by applying it to e.g., the default login shell) but I didn’t bother myself.
Once the container is started, either by you calling container.start()
or by annotating the definition with @Container
in a @Testcontainers
-annotated test class, you can run the prover like so:
String command = String.format(
"""
INFILE="/tmp/in.v" \
&& echo -e "%s" > ${INFILE} \
&& eval $(opam env) \
&& rocq c ${INFILE} \
""",
input.replaceAll("\"", "\\\""));
Container.ExecResult execResult = execInContainer("bash", "-c", command);
I’m using rocq c
(“c” for “compile”) rather than rocq repl
:
rocq repl
always returns an exit code of 0, even after encountering an error (understandable, it’s a REPL)rocq repl
emits substantial whitespace and formatting (also understandable)- the downside of using
rocq c
is that there appears to be no way to pipe input via stdin, hence the provision of an input file here, but that downside is outweighed (for my use case) by the return of a proper exit code and non-extraneous output formatting.
Also note that for some reason Rocq requires all input files to be suffixed with a .v
extension.
Lean
I was unable to find an official container image for the Lean prover; this is the one I came up with:
FROM debian:stable-slim@sha256:00a24d7c50ebe46934e31f6154c0434e2ab51259a65e028be42413c636385f7f
USER root
RUN apt-get update
RUN apt-get install curl -y
RUN apt-get clean
# create a non-root user
RUN useradd -m lean
USER lean
WORKDIR /home/lean
SHELL ["/bin/bash", "-c"]
# make sure binaries are available even in non-login shells
ENV PATH="/home/lean/.elan/bin:/home/lean/.local/bin:$PATH"
RUN curl https://elan.lean-lang.org/elan-init.sh -sSf | sh -s -- -y --default-toolchain none
RUN elan --version
# Use a stable release of Lean.
RUN elan default leanprover/lean4:v4.18.0
# Invoke lake (Lean's pkg manager) to trigger elan to install it.
RUN lake --version
# Invoke lean to trigger elan to install it.
RUN lean --version
Notes:
- I would have used alpine, but Lean doesn’t work with musl, it requires glibc.
lean
appears to only be installed at time of first invocation; in other words, omitting thelean --version
here (or any use of thelean
binary, regardless of provided flags) will defer installation oflean
to its first invocation when the container is running, rather than pre-installing it at build-time into the image.
Assuming this Dockerfile
is placed in src/{main,test,testFixtures}/resources
, creation of the container via Testcontainers looks like this:
GenericContainer<?> container = new GenericContainer<>(
new ImageFromDockerfile()
.withFileFromClasspath("Dockerfile", "com/yourpackage/Dockerfile"))
.withCommand("sleep", "infinity")
.waitingFor(Wait.forSuccessfulCommand("lean --version"));
Once the container is started, either by you calling container.start()
or by annotating the definition with @Container
in a @Testcontainers
-annotated test class, you can run the prover like so:
String command = String.format("echo -e \"%s\" | lean --stdin --json",
code.replaceAll("\"", "\\\""));
Container.ExecResult execResult = execInContainer("bash", "-c", command);
Thankfully, the Lean prover allows input from stdin, and optionally formats output in JSON when the --json
flag is provided.