Generating static and portable executables with OCaml

Hi everyone,

It has been a few times now that we have been tasked to generate portable binaries for different projects. Over time, we have gathered quite some know-how and, seeing the question frequently arise in the community, we decided to share this experience.

You can find the article written by Louis Gesbert on the OCamlPro blog

Distributing OCaml software on opam is great (if I dare say so myself), but sometimes you need to provide your tools to an audience outside of the OCaml community, or just without recompilations or in a simpler way.

However, just distributing the locally generated binaries requires that the users have all the required shared libraries installed, and a compatible libc. It’s not something you can assume in general, and even if you don’t need any C shared library or are confident enough it will be installed everywhere, the libc issue will arise for anyone using a distribution based on a different kind, or a little older than the one you used to build.

There is no built-in support for generating static executables in the OCaml compiler, and it may seem a bit tricky, but it’s not in fact too complex to do by hand, something you may be ready to do for a release that will be published. So here are a few tricks, recipes and advice that should enable you to generate truly portable executables with no external dependency whatsoever. Both Linux and macOS will be treated, but the examples will be based on Linux unless otherwise specified.

Don’t hesitate to share your thoughts with us, have a good reading!

40 Likes

Thanks for this awesome tutorial. Very useful. I tried out the steps outlined on the blog post. Most specifically, I tried getting a fully static fserv.exe executable via the ocamlpro/ocaml:4.12 (alpine based) docker image.

I have only partially succeeded in getting a fully static executable. However, given that I’m using a docker image, I should be able to repeat the example on your blog perfectly. Unfortunately that does not happen…

$ docker run --rm -it ocamlpro/ocaml:4.12
$ sudo apk add openssl-libs-static
$ mkdir fserv
$ cd fserv
$ # add the files fserv.ml, fserv.opam, dune as show in the blog post. 
$ # The dune file has (flags (:standard -cclib -static-pie)) in the executable stanza
$ opam switch create . ocaml-system --locked --deps-only
$ opam exec dune build
$ ldd _build/default/fserv.exe
	/lib/ld-musl-x86_64.so.1 (0x7fc1f787c000)

So -static-pie is statically linking in libssl, libev, libcrypto but the executable is still dynamically linked with the musl dynamic loading library which is confusing.

How do I make fserv.exe a fully static executable? I’ve tried playing around with -static etc. – nothing seems to give me fully static executable.

P.S. If I omit (flags (:standard -cclib -static-pie)) then I get:

$ ldd _build/default/fserv.exe
	/lib/ld-musl-x86_64.so.1 (0x7f00645d8000)
	libssl.so.1.1 => /lib/libssl.so.1.1 (0x7f0063d89000)
	libcrypto.so.1.1 => /lib/libcrypto.so.1.1 (0x7f0063b09000)
	libev.so.4 => /usr/lib/libev.so.4 (0x7f0063afb000)
	libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7f00645d8000)

Which is as expected.

2 Likes

Thanks for the like @OCamlPro – I think lost in all my verbiage of my post is a question – How can I get a truly static executable?

I have tried the exact same approach as outlined in the ocamlpro blog post using the docker container but have not been able to succeed in getting a truly static executable… Any helpful suggestions would be appreciated!

Louis is away this week, so I won’t be able to ask him how he got the -static-pie version to work until next Tuesday, but I got it to work using -static -no-pie (in the dune file, that would be (flags (:standard -cclib -static -cclib -no-pie))).

2 Likes

(flags (:standard -cclib -static -cclib -no-pie)) does the trick! Thank you!

$ ldd _build/default/fserv.exe 
/lib/ld-musl-x86_64.so.1: _build/default/fserv.exe: Not a valid dynamic program
$ file _build/default/fserv.exe 
_build/default/fserv.exe: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped

As it should be!

It would be interesting to see this is as a static PIE though – will watch this thread for future updates!

2 Likes

It would be nice to add also a chapter how to turn Ctypes/Cstruct-based executables into static ones as well.

Moreover, as a sign of times, a code using ocaml-rs as well.

1 Like

Ok, just investigated this — thanks for the feedback ! — and it’s actually pretty weird: on the executable generated in Alpine, with either -static or -static-pie, running ldd from Alpine returns:

 /lib/ld-musl-x86_64.so.1 (0x7fa42dc3d000)

But running ldd from Debian on the same binary:

        not a dynamic executable

… and the executable actually appears to work without pulling any dependency from the system. I suspect it still onboards some dynlinking bits though (the exe is quite bigger) — in doubt I decided to remove that part and focus on the -no-pie solution in the blogpost.

1 Like

This is interesting indeed! I didn’t think it was possible for ldd in two separate linux distros to report different results! Ah, well this is one less thing we can be sure of now :frowning:

I do think that static-pie is worth it because position independent executables are better from a security standpoint. But that is for another day I guess…

Thanks for your investigation!

Thanks for a great article!

If anyone is curious what a complete example looks like I published a project with Github actions to release a static binary here: GitHub - rbjorklin/throttle-fstrim

4 Likes