Skip to content

Instantly share code, notes, and snippets.

@rlefevre
Last active October 29, 2019 14:22
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rlefevre/e3db32d9915fb3d614bd0695de0a473c to your computer and use it in GitHub Desktop.
Save rlefevre/e3db32d9915fb3d614bd0695de0a473c to your computer and use it in GitHub Desktop.
How to build an elm 0.19.0 binary statically linked to musl libc using docker

Elm 0.19.0 Linux x64 statically linked binary

This document describes how to build a statically linked binary of Elm 0.19.0 for Linux x64 using docker. The binary is built using Alpine Linux in order to easily link it statically to musl libc. This is how the official Elm 0.19.0 Linux binary was built.

Why?

Why build a statically linked binary?

Elm is currently distributed using npm. For Linux x64 (but this applies to any architecture), this requires to have a single x64 binary that works on all Linux x64 distributions. This is considerably easier to achieve by building a statically linked binary that will only depend on the Linux kernel ABI and System Call Interface but not on userpace libraries (see here for a compatibility survey of a dynamically built executable).

Why use docker?

Docker allows to automate and reproduce the build on any system that supports docker without creating some dependencies with the host system (in our case mainly the C libraries needed by elm, and particularly the libc). This lowers the requirements to rebuild elm, improves the builds reliability and allows to manage the whole build procedure in a version control system.

Why use Alpine Linux and musl libc?

Glibc is not really suitable for static linking as it uses some dynamically loaded name resolution libraries that complicate static linking considerably (see this FAQ and NSS documentation for more information).

Alpine Linux is a very small Linux distribution particularly suitable for Continuous Integration images that uses the musl libc instead of glibc. The musl libc, defined on its homepage as "lightweight, fast, simple, free, and strives to be correct in the sense of standards-conformance and safe", is a nice alternative to glibc, particularly for static linking.

How?

1. Install Docker CE

Follow the procedure adapted to your platform.

For Ubuntu x64 (tested on 18.04):

$ sudo apt-get update
$ sudo apt-get install apt-transport-https ca-certificates curl software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
$ sudo apt-get install docker-ce
$ sudo systemctl start docker

Then optionaly (but recommanded):

  • add your user to the docker group to avoid using sudo to run the docker command:
$ sudo usermod -aG docker $USER

This will take effect after you logout/login (recommanded), or you can run su - $USER for it to take effect without re-logging, but only in the curent shell.

  • configure docker to start at boot:
$ sudo systemctl enable docker

2. Create the elm Dockerfile

Maybe one day the docker build script could be included in elm sources but for now you have to copy/paste it manually.

Create an empty directory named for example elm-docker (naming is not important) and add a Dockerfile file in it (naming is important) with the following content:

# We use Alpine 3.7 that includes ghc 8.0.2 as at the time of writing, the last
# Alpine version 3.8 includes ghc 8.4 that is not yet supported by the `language-glsl`
# haskell library used by elm.
FROM alpine:3.7

# Install required packages
RUN apk add --update ghc cabal git musl-dev zlib-dev ncurses-dev ncurses-static

# Checkout elm compiler (using 0.19.0 tag)
WORKDIR /tmp
RUN git clone -b 0.19.0 https://github.com/elm/compiler.git

# Build a statically linked elm binary
WORKDIR /tmp/compiler
RUN cabal update
RUN cabal sandbox init
RUN cabal install --only-dependencies
RUN cabal configure --disable-executable-dynamic --ghc-option=-optl=-static --ghc-option=-optl=-pthread
# For yet unknown reasons, compilation may fail when using several jobs at once
RUN cabal build --jobs=1

Take note of the comment above the git command. You can change some git options there if you want to use another elm tag/commit/branch.

3. Build the docker image including the statically linked elm binary

In the directory containing the Dockerfile, run:

$ docker build -t elm .

The -t elm option is used to name the docker image "elm", which will be useful to refer to it later.

The steps automatically executed are:

  • fetch and run the Alpine Linux image inside a container
  • install the Alpine packages required to build elm
  • build the haskell libraries required to build elm
  • build elm

If this goes well, this should end after a few minutes with something like:

Linking dist/build/elm/elm ...
Removing intermediate container ec629eeec5a1
 ---> c2a967867158
Successfully built c2a967867158
Successfully tagged elm:latest

Your new image should now be listed when running docker images in addition to the Alpine one used as our basis, for example:

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
elm                 latest              c2a967867158        2 hours ago         1.75GB
alpine              3.7                 791c3e2ebfcb        5 weeks ago         4.2MB

Note that this image is not optimized for Continuous Integration of software written in elm as it includes all dependencies needed to build elm itself. We could make an image a lot smaller for this other purpose.

4. Retrieve the elm binary

You can now retrieve the statically linked elm binary from the docker image.

As the elm compiler repository has been checked out in the /tmp image directory and built there, you can copy the elm binary from the image container to the current directory using:

$ docker run elm cat /tmp/compiler/dist/build/elm/elm > elm

You can then add execution permissions and try the binary:

$ chmod +x ./elm
$ ./elm repl

Note that the elm binary is not stripped during the build to ease debug. It could be stripped later with strip -s elm to reduce its size before distributing it (removing symbols from the object file).

5. Optional: rebuild a different elm git commit

To rebuild another version of elm manually, run the image inside a container:

$ docker run -it --rm elm /bin/ash

This opens a shell inside the container in /tmp/compiler, and you can then run some git commands (to checkout a commit/tag/branch for example) and rebuild elm if you want, for example:

/tmp/compiler# git pull
/tmp/compiler# cabal build

To retrieve the elm binary from a running container (from another shell), first get the container name from the host:

$ docker ps

Example:

CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS              PORTS               NAMES
7c4680e00158        elm                 "/bin/ash"          About a minute ago   Up About a minute                       laughing_poitras
                                                                                                                         ^^^^^^^^^^^^^^^^

then, still from the host:

$ docker cp CONTAINER_NAME:/tmp/compiler/dist/build/elm/elm .

For example:

$ docker cp laughing_poitras:/tmp/compiler/dist/build/elm/elm .

Important: If you exit the shell inside the container, all your modifications will be lost. Therefore retrieve the files you want before exiting the shell (or learn how to commit your changes into a new image).

@rlefevre
Copy link
Author

0.19.1 version is here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment