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.
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).
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.
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.
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 thedocker
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
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.
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.
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>
--------------------------------------------------------------------------------
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).
/bin/ash
should probably be/bin/bash
?