I’ve done great progress towards building truly statically linked binaries with OCaml. In a nutshell:
Switch to 4.10.0+musl+static+flambda;
Install Musl on my host, in my case version 1.2.2 which is > 1.1.24 I’ve seen mentioned related to static building;
Symlink from /usr/include/linux/ to missing /usr/include/x86_64-linux-musl/linux/;
Tell Dune to (ocamlopt_flags (:standard -ccopt -static));
Unfortunately, some packages like ppx_deriving and ocaml-protoc-yojson fail to install in static switches. I’d like to understand why and if it could be worked around, so I dug into ocaml-protoc-yojson's case. Here’s the output from opam:
I grepped the repo of ocaml-protoc-yojson and there is no -shared in any file. Did opam (2.0.7) add that option to ocamlopt somehow? If so, why would it do that in a static switch?
If anyone managed to work around this issue, I’d love to know how.
I’m not sure what you’re trying to achieve with -shared… but I’d just like to point out I got success building mixed ocaml/C/C++ on Alpine (from a docker container), where musl is the default libc. There’s nothing special to do other than passing -ccopt -static to the ocaml compiler. In particular:
no need to select a special opam switch
finding the C++ standard library worked out of the box (unlike on ubuntu, which is where I gave up when I tried)
See for example how it’s done for dune-deps, whose CI build runs on a docker image created with ocaml-layer. You can just run docker run -it mjambon/ocaml:alpine and cat /Dockerfile to see the setup. Demo:
$ docker run -it mjambon/ocaml:alpine
Running "eval $(opam env)", which initializes PATH and other variables.
In scripts and dockerfiles, dont forget to run your commands as
opam exec -- COMMAND
or equivalently as
eval $(opam env) && COMMAND
bash-5.0$ opam switch
# switch compiler description
-> 4.10.0 ocaml-base-compiler.4.10.0 4.10.0
default ocaml-system.4.08.1 default
bash-5.0$ cat > toto.ml
Printf.printf "%f\n" (Unix.gettimeofday ())
bash-5.0$ ocamlopt -o toto -ccopt -static unix.cmxa toto.ml
bash-5.0$ ldd toto
/lib/ld-musl-x86_64.so.1 (0x7f12214ae000)
bash-5.0$ file toto
toto: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped
bash-5.0$ ./toto
1612212056.085955
If you don’t want a position-independent executable (PIE) static binary (not sure when this is needed), on Alpine you have to pass -ccopt -no-pie:
bash-5.0$ ocamlopt -o toto-no-pie -ccopt -static -ccopt -no-pie unix.cmxa toto.ml
bash-5.0$ ldd toto-no-pie
/lib/ld-musl-x86_64.so.1: toto-no-pie: Not a valid dynamic program
bash-5.0$ file toto-no-pie
toto-no-pie: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped
There are no issues with installing the packages you mentioned:
Thanks for this detailed response! FYI I’m not the author of ocaml-protoc-yojson, I’m just wondering why there’s a -shared in opam’s error log since it’s not in the project’s Makefile. That said:
I think this means that I was mistaken about the meaning of those +static switches to begin with. Those seem to be about the compiler’s own building and not its output. I’ll try the same with 4.11.1+musl+flambda directly on my host (no Docker) as a test… yup, everything installs fine! Building statically… yup:
$ file basics.exe
basics.exe: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped
$ ldd basics.exe
not a dynamic executable
So the lesson here is that one needs a +musl build, not +static, to build statically (in addition to -ccopt -static of course).