.. 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.