Skip to content

Instantly share code, notes, and snippets.

@rlefevre
Last active November 22, 2021 15:27
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rlefevre/1523f47e75310e28eee243c9c5651ac9 to your computer and use it in GitHub Desktop.
Save rlefevre/1523f47e75310e28eee243c9c5651ac9 to your computer and use it in GitHub Desktop.
How to build an elm 0.19.1 binary statically linked to musl libc using docker

Elm 0.19.1 Linux x64 statically linked binary

This document describes how to build a statically linked binary of Elm 0.19.1 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.1 Linux binary is 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. Download elm Dockerfile

You can get the current Dockerfile from the elm compiler repository:

https://raw.githubusercontent.com/elm/compiler/master/installers/linux/Dockerfile

Create an empty directory named for example elm-docker (naming is not important) and add the Dockerfile file in it (naming is important). Or copy/paste this one:

FROM alpine:3.10

# branch
ARG branch=master
# commit or tag
ARG commit=0.19.1

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

# Checkout elm compiler
WORKDIR /tmp
RUN git clone -b $branch https://github.com/elm/compiler.git

# Build a statically linked elm binary
WORKDIR /tmp/compiler
RUN git checkout $commit
RUN rm worker/elm.cabal
RUN cabal new-update
RUN cabal new-configure --disable-executable-dynamic --ghc-option=-optl=-static --ghc-option=-optl=-pthread --ghc-option=-split-sections
RUN cabal new-build
RUN strip -s ./dist-newstyle/build/x86_64-linux/ghc-8.4.3/elm-0.19.1/x/elm/build/elm/elm

Take note of the branch and commit arguments. You can change them 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
  • update cabal packages list
  • build elm

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

Linking /tmp/compiler/dist-newstyle/build/x86_64-linux/ghc-8.4.3/elm-0.19.1/x/elm/build/elm/elm ...
Removing intermediate container ad1b987bdc71
 ---> d86c379f3f00
Successfully built d86c379f3f00
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              d86c379f3f00        About a minute ago   2.08GB
alpine              3.10                4d90542f0623        2 weeks ago          5.58MB

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 create elm
3ec04a99a4839ba5abb29811d6ece68a2528faac84233d6512ac4c211b1cdb37

# Then use the previous container ID
$ docker cp 3ec04a99a4839ba5abb29811d6ece68a2528faac84233d6512ac4c211b1cdb37:/tmp/compiler/dist-newstyle/build/x86_64-linux/ghc-8.4.3/elm-0.19.1/x/elm/build/elm/elm .

or in one command:

$ docker cp $(docker create elm):/tmp/compiler/dist-newstyle/build/x86_64-linux/ghc-8.4.3/elm-0.19.1/x/elm/build/elm/elm .

You can then add execution permissions and try the binary:

$ chmod +x ./elm
$ ./elm repl
---- Elm 0.19.1 ----------------------------------------------------------------
Say :help for help and :exit to exit! More at <https://elm-lang.org/0.19.1/repl>
--------------------------------------------------------------------------------

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 new-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-newstyle/build/x86_64-linux/ghc-8.4.3/elm-0.19.1/x/elm/build/elm/elm .

For example:

$ docker cp laughing_poitras:/tmp/compiler/dist-newstyle/build/x86_64-linux/ghc-8.4.3/elm-0.19.1/x/elm/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).

@Janiczek
Copy link

/bin/ash should probably be /bin/bash?

@rlefevre
Copy link
Author

rlefevre commented Jul 19, 2019

Nope, Almquist shell, alias ash, is a small shell used by Alpine Linux and BusyBox among others:

https://en.wikipedia.org/wiki/Almquist_shell

@Janiczek
Copy link

TIL! Thanks for clarifying 🙂

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