Resolving Issue #1949 (how to build Elm) with improved dockerfiles?

Problem

I am new to the Elm community (this is my first post) and have zero open source contribution experience, but think Issue #1949 could be resolved by improving the current Dockerfile in Elm compiler source. My hypothesis does require new build practices to be followed by Elm maintainers. As such, I hope this community post is an appropriate way to gauge feasability and interest.

Hypothesis

Elm compiler source dockerfiles used to distribute official elm binaries are instructively transparent for community builders. Given the Dockerfile format is human-and-machine readable (like Markdown), maintainers could answer questions like "How do I bulid elm@x.y.z on distro abc" by pointing to Dockerflie.abc in git clone --branch x.y.z https://github.com/elm/compiler.git. The main benefit over regular instructions is that automated Docker Hub builds could empirically show which Dockerfile instructions succeed or for what reasons they fail. (For instance, not having updated ca-certificates leads to HTTP.elm compilation problems.)

Background

The current compiler/installers/linux/Dockerfile successfully builds a static elm binary. I’ve tried to improve upon this base in three different dockerfiles called Dockerfile, Dockerfile.alpine, and Dockerfile.debian:

  1. Dockerfile copies an amd64 static binary to an empty docker image for reduced size and easier extraction.
  2. Dockerfile.alpine copies a non-static amd64 binary onto a Dockerhub Node image for use in amd64 CI/CD.
  3. Dockerfile.debian copies a non-static multiarch binary onto Docker Hub Node for use in multiarch CI/CD or to extract multiarch binaries for ChromeOS 80’s Buster based Linux container

All three versions COPY source code over using git, which makes automated Docker Hub builds a bit faster. In addition, I think these non-git-based-dockerfiles could resolve ‘Issue #1949: requests for “build from source” instructions’ by showcasing how to build and deploy the tagged branch of Elm they reside in. (The idea being if you want to build 0.19.2 or 0.20.0 you’d inspect their dockerfiles for hints like ‘does it use stack?’, ‘does it use cabal?’, or ‘where did the automated Docker Hub build fail/pass between version bumps?’)

1 Like

Hi and welcome to the Elm community.

As the author of installers/linux/Dockerfile and a proponent of the x64 Linux binary statically linked to musl, I can answer a few points from my own point of view.

First the Dockerfile uses git because it existed outside the sources before (for 0.19.0 then for 0.19.1), and was imported as is during 0.19.1 release. But you can notice that there is a PR that uses the docker build context instead, adds a Travis CI build on master, decreases the binary size significantly and automates Linux x86 tagged releases. I think this Dockerfile with the CI is valuable because:

  • It allows to build a binary that works on any Linux x86_64 distribution.
  • It allows to anticipate build issues on Alpine Linux before releases.
  • It reduces the burden during releases.
  • It offers some guaranties about how the release was built.
  • It provides a recipe to rebuild old versions, which can be useful for companies that maintain applications for a long time without updating elm version, and eventually need a critical fix.

About the three different Dockerfile using Cabal, we really have to answer what this tries to achieve.
If this is to solve #1949, let’s look at the referenced issues inside and note that they were opened before the Dockerfile was merged in the sources. Also:

  • #1894 and #1948 are about instructions to build elm, the answers are with stack
  • #1892 is about building elm on FreeBSD
  • #1878 is about building elm on Windows 32 bits

So as far as I can tell, none of them would have been solved by the three different Dockerfile, and those Dockerfile would have to be maintained, or they would become broken after a while.

What could have eventually helped might be to have a build based on stack but Elm author uses cabal.

Moreover #1949 is about difficulties to keep up with ever changing haskell platform, and there are already Linux x64, MacOS X and Windows 64 builds to maintain, so I’m not sure how adding new ones will help with the issue.

If this is about Linux distributions support, there are a few points to consider:

  • this was the whole point of having a statically linked binary that works everywhere, to avoid this
  • adding Debian and a dynamically linked Alpine build won’t help much to build elm on the tens of Linux distributions versions used by elm users
  • elm has about a new release a year, but most distributions like Debian have a longer lifecycle, and those that don’t, like Arch often use the statically linked Linux binary as is.

But more generally, this is about the way of distributing Elm, which is not a solved issue, because:

  • elm binary alone is not really useful. For CI or developers, at minimum elm, elm-format, elm-test (and therefore elmi-to-json) are needed and very often also elm-verify-examples, elm-analyse and elm-xref because they are used to validate the source code and builds. So solving only the elm case is not enough.
  • users use different versions of Elm, some companies still use 0.18, or even 0.17, and sometimes several versions simultaneously
  • elm is used in web projects that often include some backend or bundling, so it is most often installed using npm, along the other elm tools. Therefore adding a way to install it using the distributions packaging would not solve the npm issues with permissions (particularly on Linux) and binaries handling (particularly on Windows).

Sorry for the quite messy answer, but this is actually a more complex subject that it seems. Hopefully this gives a few pointers to help understanding the current situation.

To go forward:

  • What are you really trying to solve ?
  • Do the 3 different Dockerfile really help more than the one already included? If so how?
  • You talk about “Elm maintainers” and “community builders”, who are they and do you have sources to corroborate your hypothesis (for example which maintainer asked "How do I build elm@x.y.z on distro abc ")?
1 Like

On my post ambiguities ("How do I build elm@x.y.z on distro abc" and more)

First off, sorry for the confusion! I was trying to explain myself in as succinct a manner possible, and clearly I didn’t get the balance right. Thank you @dmy for pointing that out. (It is is amazing to talk to you, given your Dockerfile is what got me interested in docker.)

I wrote “Elm maintainers” and “community builders” because I had no idea how Elm is maintained or if community builders are out there. I knew from Issue #1949 that people have expressed interest in compiling elm, so I refer to them as ‘community builders’. I know Elm is open source, but people like evancz and NoRedInk seem to be in charge so I referred to them (or people with the same authority over source code) as ‘Elm maintainers’.

On Issue #1949

I think the core of Issue #1949 is that it is hard for evancz to provide build instructions for mac, windows, and linux in a general and timeless fashion. Clearly I need to edit my post, because re-reading it sounds like I am proposing the creation of Dockerfile.debian, Dockerfile.centos, and more. (That’s a whole lot of work that I would not want to do!) In truth, I think you’d only need to maintain Dockerfile.musl-linux, Dockerfile.windows, and Dockerfile.glibc-linux (See Do different Dockerfiles help? Maybe! below for more details).

Getting back to Issue #1949 though, the point I was trying to make is that I learned how to build Elm from @dmy’s Dockerfile. That pretty amazing to me! I can only compare the experience of learning how to build Elm from a Dockerfile to how easy it is to write markup in Markdown. It showed me I should use cabal-install if I can, and led me down a rabbit hole where I spent a day trying to build Elm with packaged dev-tools in Cent OS, Ubuntu, Debian, etc.

What I’m really trying to solve

Based on this experience, I was wondering if maintaining a small number of Dockerfiles would provide the community enough hints to build versions of Elm on many platforms in a timeless fashion. (Timeless in the sense that, as @dmy points out, a repo Dockerfile provides “some guarantees of how a release was built”.)

It’s not a perfect solution for everyone since people will still run into obscure platform specific issues. Still, it could be a good jumping off point worth generalizing towards, right?

Do different Dockerfiles help? Maybe!

Warning: this section is a hodge-podge…

The main benefit of Dockerfile.glibc-linux is that it would be multi-arch support today, but I can see how that’s a bit short-sighted when ghc and cabal might get multi-arch support in Alpine Linux tomorrow. Still, if we used debian:stretch as a base most linux distros have libatomic and libtinfo5 packaged, and those are the only two library requirements neccessary to run Elm dynamically linked (in my limited experience).

I haven’t tried to build Dockerfile.windows yet (and I don’t know if Dockerfile.mac is technically possible with public image repos), but they might provide a similar ‘builder learning experience’ to the one I had with @dmy’s Dockerfile.

Multi-stage dockerfiles might also be interesting because they can be generically executed by many CI solutions like travis, gitlab-runner, and jenkins. Simply set up builder, tester, and production stages in a dockerfile. (You can even use a multi-stage dockerfile to encapsulate Dockerfile.static-linux, Dockerfile.glibc-linux, and Dockerfile.windows into one Dockerfile, but that’s only useful if all dockerfiles involved target the same cpu architecture.)

PR #2033 notes

(Thank you @dmy for the background info and for converting the dockerfile to use COPY instead of git. Here are some additional things I noticed while messing around with the file that might be helpful)

  1. cabal has library and binary stripping enabled by default, so I don’t think you need RUN strip -s /usr/local/bin/elm. Along the same vein --disable-executable-dynamic is on by default according to cabal docs, so you probably don’t need the flag.
  2. I don’t think you need --ghc-option=-optl=-pthread to make a static binary with cabal-install anymore. I remember reading in a forum somewhere that they fixed the pthread issue for static binaries. (I’m guessing if it were important it would be part of the new executable-static config option. The cabal-install docs say executable-static only passes -optl=-static and -static to GHC, which is already done by the current flag --ghc-option=-optl=-static. Based on this, I think you can safely use pthreading now.)
  3. I recommend adding ca-certificates to packages downloaded via apk add --no-cache. (Outdated certificates can cause builds with HTTP.elm to fail because it cannot recognize updated security certificates.) Alpine doesn’t seem to update packages automatically unless specified in my experience, even if you use apk add --update.

Builds for other systems than Linux

Evan uses MacOS X, and the release installer must be notorized anyway, so automatizing the Mac build does not seem that useful. However I think that having an automated build for Windows could definitely be interesting indeed.

What could also be useful outside the elm compiler source tree would be to help with FreeBSD ports (elm and elm tools) and to help getting ghc back on Alpine armhf in case we want to build an arm static binary later (or help getting a working cross-compilation solution for an armhf musl static binary ).

Binary dynamically linked to glibc with Debian

How is this a benefit? AFAIK this means the distro is able to run binaries built from several architectrues (commonly x86 and x86_64 for example), but each binary is built for a single arch. For example your Dockerfile.debian produces the following binary:

elm: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=be9967c08aefb57385cacb0b0bb458fe0b6b8fb8, not stripped

So it is a 64-bit binary that will only work on distributions supporting x64 binaries, like the official elm one. Except this one is dynamically linked, and won’t work on a number of distributions, for example Ubuntu 18.04 LTS:

./elm: error while loading shared libraries: libtinfo.so.6: cannot open shared object file: No such file or directory

You can find here a report of compatibility issues with such binaries. This is why using a binary statically linked to musl has been the preferred way for Linux since elm 0.19.0 alpha.

First, stretch only has ghc 8.0, which won’t build elm. So you would have to import another haskell-platform. But even then, you will still have some compatibility issues with some distros (ncurses-compat sometimes helps, but not always, and users won’t know they must install it), and the binary won’t work on Alpine. You can try it by yourself and make a result table like the one linked above. The official elm binary works on distros as old as Debian 4 etch with a 2.6 kernel.

So unless some performance differences are found between glibc and musl for elm, it seems that the only benefit would be eventually the size of the binary, but from my tests this is not even the case, as I can reduce the Debian binary to 18MB (with proper split and strip, see below) vs 14MB for the statically linked one.

Multi-stage Dockerfile

It might be useful, but how it will be useful to Elm maintainers?
I don’t want to put anything unused in the Dockerfile.

Also isn’t it easier (for later modifications) and faster to build CI images from the binary like in this PR?

Strip & split-sections

I think this only applies to binaries installed with cabal install, not those built with cabal.
You can clearly see that your binaries are not stripped (note the “not stripped at the end”):

$ file elm
elm: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped

Similarly, your --enable-split-sections will only split sections for the final binary, not for the dependencies (libraries), so you will lose most of the benefit. That’s why your binary is a 55MB one vs 14MB for the one with the Dockerfile from my PR.

-optl=-pthread

Good to know. Do you have a link to the source so I can check this applies for the version of cabal-install used on Alpine?

--no-cache & ca-certificates

I agree --no-cache should be better, I will use it. Adding ca-certificates seems like a good idea too, but I have never seen an error without updating it. Do you have an example? Also what is HTTP.elm?

Thanks for these notes about the PR, improvements are always welcome. I will update my PR.

Glibc from Debian is probably a bad idea

You’re right, I forgot for Stretch I had to use ghcup on Debian to build Elm. That makes a dynamically linked Debian build only really useful to the ChromeOS ARM64 Buster Linux Container (which isn’t out yet and probably a small use case which really only affects me.). Thank you for the feedback @dmy.

(PS: I am only able to provide two links as a new Discourse user, so I’ll be breaking up my responses into multiple posts because its easier to link you examples. Hope it doesn’t make the thread too chaotic… or DoS ya with forum updates :sweat_smile:)

On CI based on PR #2044 (docker composition)

From my perspective, PR #2044’s needs a public Elm docker image running if it wants to provide a good example of docker composition for end users. Docker composition seems most useful when your inputs are all docker images. Since Elm is only distributed as a tar archive, you are better off teaching people how to make simple dockerfiles which follow the current Elm install process. As an example, this Dockerfile has the same behavior as the current file changes in PR #2044.

Making a public docker image from PR #2033 for PR #2044

Currently PR #2033’s Dockerfile puts the Elm binary at /usr/local/bin/elm, which is easy for experienced Linux developers to find. That said, an Elm binary in a scratch docker image might be easier to find because there would be only one file at /elm. The tiny image size also makes it an ideal candidate for use with docker compose.

Nix is another technology that aims to provide reproducible builds from source. Its expression for building elm is here.