Login

Highly-portable executables with Alpine Linux

There's Cross-compile with Zig but that gets to be more of a hassle as you use more C libraries that aren't distributed as opam packages, and currently OCaml isn't able to benefit from Zig's ability to also cross-compile across CPU architectures and OSes. If the only benefit is more-portable binaries within Linux targets of the same architecture, then we get that benefit with more flexibility and less hassle from building with Alpine Linux.

The most popular way to do that is with Docker. Docker has very many serious and practical advantages when you get going with it, but in the early stages it's quite a pain. Thus,

Distrobox

Get it from https://distrobox.it/ or dnf install distrobox, and then:

$ distrobox create --name alpine --image alpine:latestImage alpine:latest not found.
Do you want to pull the image now? [Y/n]: 
Resolved "alpine" as an alias (/etc/containers/registries.conf.d/000-shortnames.conf)
Trying to pull docker.io/library/alpine:latest...
Getting image source signatures
Copying blob 014e56e61396 done   | 
Copying config 7acffee03f done   | 
Writing manifest to image destination
7acffee03fe864cd6b88219a1028855d6c912e7cf6fac633aa4307529fd0cc08
Creating 'alpine' using image alpine:latest      [ OK ]
Distrobox 'alpine' successfully created.
To enter, run:

distrobox enter alpine

$ distrobox enter alpine
Starting container...                            [ OK ]
Installing basic packages...                     [ OK ]
Setting up devpts mounts...                      [ OK ]
Setting up read-only mounts...                   [ OK ]
Setting up read-write mounts...                  [ OK ]
Setting up host's sockets integration...         [ OK ]
Integrating host's themes, icons, fonts...       [ OK ]
Setting up distrobox profile...                  [ OK ]
Setting up sudo...                               [ OK ]
Setting up user's group list...                  [ OK ]
Setting up existing user...                      [ OK ]
Ensuring user's access...                        [ OK ]

Container Setup Complete!
📦$ sudo apk add opam
(1/5) Installing bubblewrap (0.11.0-r2)
(2/5) Installing bubblewrap-doc (0.11.0-r2)
(3/5) Installing bubblewrap-bash-completion (0.11.0-r2)
(4/5) Installing opam (2.5.0-r0)
(5/5) Installing opam-doc (2.5.0-r0)
Executing busybox-1.37.0-r29.trigger
OK: 504 MiB in 379 packages
📦$ sudo apk add build-base m4 perl bash ncurses-dev zlib-dev gmp-dev libffi-dev python3 curl tar
📦$ opam switch create alpine 5.4.0

Note that distrobox is not hermetically sealed from the host OS, so 'opam switch' will actually show your existing switches. This introduces some possibility for error but also enormously simplifies casual use.

With the alpine switch you can create static executables with -ccopt static, but you may be unpleasantly surprised when you run them:

$ ./linux.musl.static 
Segmentation fault (core dumped)

By default, for ASLR, Alpine Linux builds position-independent executables that still require a .so loader. You could hypothetically ship the loader with the binary, but I couldn't find a way to do that (like -Wl,--dynamic-linker=./ld-musl-x86_64.so.1) that didn't still segfault. You can drop the protection with -no-pie

📦$ ocamlopt.opt o19.ml -ccopt -static -ccopt -no-pie -o linux.musl.static
$ ./linux.musl.static 
22
$ ldd linux.musl.static 
        not a dynamic executable

More a more motivating example, lets get a portable binary with openssl:

📦$ sudo apk add gmp-static openssl-libs-static 
📦$ opam install dune ssl
📦$ dune init proj checkcert

For the bin/dune file:

(executable
 (public_name checkcert)
 (name main)
 (ocamlopt_flags -ccopt -static -ccopt -no-pie)
 (libraries o31 ssl unix x509))

and bin/main.ml:

let () =
  Ssl.init ();
  let addr =
    Unix.getaddrinfo Sys.argv.(1) "443" [AI_SOCKTYPE SOCK_STREAM]
  in
  let sockaddr = (List.hd addr).ai_addr in
  let ssl = Ssl.open_connection Ssl.TLSv1_3 sockaddr in
  let cert = Ssl.get_certificate ssl in
  let cipher = Ssl.get_cipher ssl in
  Printf.printf "SSL connection ok.\n";
  Printf.printf "Certificate issuer: %s\nsubject: %s\n" (Ssl.get_issuer cert)
    (Ssl.get_subject cert);
  Printf.printf "Cipher: %s (%s)\n%s"
    (Ssl.get_cipher_name cipher)
    (Ssl.get_cipher_version cipher)
    (Ssl.get_cipher_description cipher);
  Ssl.shutdown ssl;
  print_newline ()

We can get a static executable that prints some cert information with openssl:

📦$ dune exec checkcert ocaml.org
SSL connection ok.                
Certificate issuer: /C=US/O=Let's Encrypt/CN=E8
subject: /CN=v3.ocaml.org
Cipher: TLS_AES_256_GCM_SHA384 (TLSv1.3)
TLS_AES_256_GCM_SHA384         TLSv1.3 Kx=any      Au=any   Enc=AESGCM(256)            Mac=AEAD

$ ldd ./_build/default/bin/main.exe
        not a dynamic executable
$ ./_build/default/bin/main.exe dune.build
SSL connection ok.
Certificate issuer: /C=GB/ST=Greater Manchester/L=Salford/O=Sectigo Limited/CN=Sectigo RSA Domain Validation Secure Server CA
subject: /CN=*.github.io
Cipher: TLS_AES_128_GCM_SHA256 (TLSv1.3)
TLS_AES_128_GCM_SHA256         TLSv1.3 Kx=any      Au=any   Enc=AESGCM(128)            Mac=AEAD