.. post:: 2017-11-03
:tags: OCaml, Docker
:author: Rudi Grinberg
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:
.. code-block:: diff
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.