Creating Static Linux Binaries in OCaml¶
Creating truly static binaries for Linux like golang is a capability that is occasionally useful. I’ve seen questions about it on IRC a few times, and I’ve personally found this approach is particularly useful when deploying to environments where installing libraries isn’t easy, such as AWS Lambda. Unfortunately for me, the approach that I will explain in this article wasn’t as approachable. So I’ve prepared a quick tutorial on how to easily create a static binary in OCaml and test it.
But first of all, let me briefly describe the problem of creating such binaries
in OCaml. Which actually has nothing to do with OCaml itself, and everything to
do with C libraries. OCaml (ocamlopt
) itself always links in all OCaml code
statically but defaults to dynamically linking C libraries because it’s somewhat
of a standard, and glibc doesn’t support static linking.
In this post we’ll focus on solving the problem with statically linking C libraries. The solution here is simple, we find an alternative to glibc that supports static linking. That alternative is musl. Now we simply need to find or create a musl based build tool chain. Luckily for us, this will be exceedingly easy as @avsm maintains a set of docker images with opam installed in Alpine Linux. A Linux distribution that is entirely musl based.
Hence all we need to do is to build out binaries on an Alpine image, and pass some flags to our build system to request static linking.
So let’s fire up an Alpine container with opam installed:
$ docker run --rm -ti ocaml/opam:alpine_ocaml-4.05.0 bash
Let’s make sure opam is up to date:
$ opam update
Now we need to install m4. This is required for findlib.
$ sudo apk add m4
Next we need to install ssl’s external dependencies. This can easily be done with:
$ opam depext ssl
We can now install OCaml’s ssl bindings:
$ opam install ssl
For our example, we’ll use an ssl based, command line, http client from cohttp-lwt-unix. So let’s install its dependencies:
$ opam install --deps-only cohttp-lwt-unix.0.99.0
Of course, to produce a static binary we need to modify the build system. So we’ll get the source with:
$ opam source cohttp-lwt-unix.0.99.0 && cd cohttp-lwt-unix.0.99.0
With the following patch which passes the necessary gcc flags to produce a static binary:
diff --git a/cohttp-lwt-unix/bin/jbuild b/cohttp-lwt-unix/bin/jbuild
index 7e562da..fa58b66 100644
--- a/cohttp-lwt-unix/bin/jbuild
+++ b/cohttp-lwt-unix/bin/jbuild
@@ -3,5 +3,6 @@
(executables
((names (cohttp_curl_lwt cohttp_proxy_lwt cohttp_server_lwt))
(libraries (cohttp-lwt-unix cohttp_server cmdliner))
+ (flags (-ccopt -static))
(package cohttp-lwt-unix)
(public_names (cohttp-curl-lwt cohttp-proxy-lwt cohttp-server-lwt))))
It can be applied to our source as follows:
$ patch -p1 < static-patch
Let’s build our binary:
$ jbuilder build -p cohttp-lwt-unix cohttp-lwt-unix/bin/cohttp_curl_lwt.exe
Now we’ll confirm that this binary indeed works on other Linux distros. Docker will prove to be useful for this as well. We’ll launch a Debian instance:
$ docker run --name target --rm -ti debian:latest bash
Now let’s copy our binary from the host container to the target:
$ docker cp host:/home/opam/ocaml-cohttp/_build/default/cohttp-lwt-unix/bin/cohttp_curl_lwt.exe ./cohttp_curl_lwt.exe
$ docker cp ./cohttp_curl_lwt.exe target:/root/cohttp_curl_lwt.exe
Now let’s verify our binary is indeed static:
$ ldd /root/cohttp_curl_lwt.exe
statically linked
And finally test that it works as expected:
$ /root/cohttp_curl_lwt.exe -v https://www.yahoo.com
This simple approach taken here will not work in all cases. For example, here’s a post that describes some complications a different user has encountered. Nevertheless, it should be a good start for HTTP clients and servers.