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.

Comments

comments powered by Disqus