Skip to main content

lsupg Static Builds With GHC 9 (Part 5)

Static building of lsupg is now working, using a project-specific Dockerfile to create ephemeral Alpine Linux containers with GHCup builds of GHC.

The project currently uses a single Dockerfile that works with all of the supported GHC versions, using the GHC_VERSION argument to specify the version to install when the image is built. Images are tagged as lsupg-build:${GHC_VERSION}. Since the goal is to use the latest versions of dependencies, the images are meant to be ephemeral. New images should be built to make new releases. To support this goal, the alpine:latest image is used, and version numbers are not specified when installing packages. In cases where the build environment needs to be kept, you can export the image or container to save it.

The Makefile provides a command for making static builds. When using Stack, run make static CONFIG=stack-9.8.2.yaml, using the CONFIG argument to specify the compiler/configuration. If no CONFIG is specified, stack.yaml is used, which is just a link to the current LTS configuration. When using Cabal, run make static MODE=cabal GHC_VERSION=9.8.2, using the GHC_VERSION argument to specify the compiler version. If no GHC_VERSION is specified, the version of ghc in your PATH is used.

The lsupg-build image is automatically created if it does not already exist. It is up to the developer to manage the alpine:latest and lsupg-build images, however. Note that lsupg can be used to investigate if an image has pending upgrades, which is the whole purpose of the program. For example, run lsupg --docker lsupg-build:8.6.5 to check if there are upgrades available for the lsupg-build:8.6.5 image.

This was easy to get working thanks to GHCup! There was only one tricky part: passing Stack flags. The project is configured to enable static building using a static flag, which is passed via the command-line. When one or more flags are passed via the command-line, however, Stack ignores any flags that are configured in the stack.yaml file. It is therefore necessary to parse the stack.yaml configuration and pass any flags configured there via the command-line, along with the static flag. I am currently doing this using a stack-yaml-flags AWK script.

I worked through my backlog of tasks for this project and added support for GHC versions 8.6, 9.6, and 9.8. With all of the target versions now working, I checked the sizes of the (stripped) static executables.

GHC Stack Cabal
8.6.5 13,691,816 16,253,512
8.8.4 13,452,744 16,505,288
8.10.7 15,046,312 16,347,080
9.0.2 28,681,320 29,043,720
9.2.8 28,918,712 29,239,192
9.4.8 18,265,800 18,307,752
9.6.5 33,206,056 33,076,616
9.8.2 33,357,672 33,204,808

I am not sure why there is a difference in size between Stack and Cabal builds. Perhaps they are being compiled using different options. Much more interesting, however, is the small size of the GHC 9.4.8 builds. That is the version using the dynamically linked official build of GHC! I confirmed that the lsupg build is indeed static. This might be worth investigating.

I made one change to the lsupg code, adding build information to the --version output (only) when the program is built on Alpine.

$ ./build/lsupg --version
lsupg-haskell 0.3.0.2 built on Alpine 3.19.1 using ghc 9.4.8

I still need to update the README, and there may be some other things that I should do first, but I plan on making a new release soon.