Skip to main content

ghc-musl Part 4: lsupg With Cabal

Now that I am able to use ghc-musl to build fully static executables of lsupg using Stack (see ghc-musl Part 3: lsupg With Stack), it is time to get it working using Cabal. It is easy to use Cabal to build within a ghc-musl container, but some orchestration is needed so that one can just run a make command that runs a container, builds the executable, and puts the executable in an appropriate location (with the correct permissions). Stack handles all of this, but custom orchestration is required for Cabal.

Image Selection

One decision to make is how to select the Docker image to use for the static build. A simple solution would be to pass the image as an argument to the make command, but I do not like this option. When using Cabal, my Makefiles use a GHC_VERSION variable. If specifying both IMAGE and GHC_VERSION (which defaults to the version of ghc in your PATH), it is possible that they will not match. This is not a problem if the GHC_VERSION is recalculated within the container, so that the correct cabal.project file is selected, but this complicates things. More importantly, from a user perspective, I do not want to have to lookup the correct image name when I run the command. I would rather configure the images and select one by GHC version.

See cabal.project Per GHC Version (and Part 2) for discussion about the necessity of using separate cabal.project files for different GHC versions in some cases. The Stack design where the GHC version is determined by the build configuration (stack.yaml file) is really convenient. With Cabal, care must be taken to use the appropriate cabal.project file for the version of GHC being used.

So, I will use GHC_VERSION to determine which Docker image to use. Where should these images be configured? Since the images are already configured in the stack.yaml files, I considered parsing the image name from the file corresponding to GHC_VERSION when in Cabal mode. This was easy to implement, but there is an issue with this method. Cabal supports new versions of GHC as soon as they are released, but it takes some time before support for a new version is added to Stack. Is it acceptable to limit Cabal versions to those supported by Stack? I would rather not.

An alternative is to just configure the images in the Makefile. If they are configured by GHC_VERSION, then it is trivial to select an image when using Cabal mode. Unfortunately it is not straightforward when using Stack mode because the GHC version is determined by the stack.yaml file being used. I could parse the filename (and symbolic link) to determine the version, but that does not work for all Stack configuration. (I sometimes have configuration for special builds/tests that do not include the GHC version in the filename.)

Another alternative is to configure the images in the Makefile as well as in the stack.yaml files. Configuring this in multiple locations is not optimal, but an advantage is that it would then be possible to use different images for the same version of GHC depending on which tool is being used to build. (I hope that is never necessary, but it is possible.) Since this is the only method that supports everything that I (might) want to do, I will go with it.

Building

The project directory can be mounted when the ghc-musl container is run, so that the build uses the current state of all the project files. (Using Git or a source tarball would not work well.) Unfortunately, this raises the common Docker issue of file ownership. The ghc-musl containers use the root user, so any files written to the mounted directory will be owned by root. These files should be owned by the host user.

There are various solutions to the ownership problem.

  • The ownership can be fixed after the container stops. Unfortunately, this requires sudo.
  • The ownership can be fixed within the container. The user UID and GID can be passed as arguments, or a common shortcut is to use the UID and GID of the mounted directory.
  • The container can use a non-root user, and the UID and GID of that user can be changed appropriately when the container starts. This is the approach taken by the fixuid utility. Note that this would require significant changes to the ghc-musl images.
  • The image can be built using a non-root user with the appropriate UID and GID set from the beginning. This is what I do in the docker-ghc and docker-pkg projects. Note that this only works for people who build their own images, so it is not appropriate for the ghc-musl images, which are hosted on Docker Hub.

I think it would be a huge improvement to not build as root, but I am currently not considering the option of significantly changing ghc-musl. I prefer to avoid the use of sudo (on the host), so I will go with the second option.

I tried it out manually before writing a script, and everything is as expected. Note that I call cabal v2-build with the --enable-executable-static option as well as set the static flag that is configured in lsupg.cabal. This project currently uses a common cabal.project file that works with all supported GHC versions, so a version-specific cabal.project file does not need to be specified. The built executable is not stripped, so this needs to be done as well. Setting my (1331) UID and GID works without issue.

[lsupg] $ docker run --rm -it -v $(pwd):/host \
  extremais/ghc-musl:v23-ghc922 /bin/bash
bash-5.1# ls -ld /host
drwxr-xr-x    9 1331     1331          4096 Mar 25 02:25 /host
bash-5.1# cd /host
bash-5.1# cabal v2-build --enable-executable-static --flags=static
...
Linking /host/dist-newstyle/.../lsupg ...
bash-5.1# ls -ld dist-newstyle/
drwxr-xr-x    6 root     root          4096 Mar 25 02:31 dist-newstyle/
bash-5.1# chown -R 1331:1331 dist-newstyle/
bash-5.1# exit
[lsupg] $ find dist-newstyle -type f -name lsupg | xargs ls -l
-rwxr-xr-x 1 tcard tcard 47572512  3月 25 11:34 dist-newstyle/.../lsupg
[lsupg] $ find dist-newstyle -type f -name lsupg | xargs file
dist-newstyle/.../lsupg: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
  statically linked, with debug_info, not stripped
[lsupg] $ find dist-newstyle -type f -name lsupg | xargs strip
[lsupg] $ find dist-newstyle -type f -name lsupg | xargs ls -l
-rwxr-xr-x 1 tcard tcard 29494152  3月 25 11:39 dist-newstyle/.../lsupg
[lsupg] $ $(find dist-newstyle -type f -name lsupg) --version
lsupg-haskell 0.3.0.1

I wrote a script to run inside a ghc-musl container, trying to keep things simple. The script just builds the executable and corrects the ownership. Stripping the executable can be handled on the host.

#!/usr/bin/env bash

set -o errexit
set -o nounset
set -o pipefail
#set -o xtrace

die () {
  echo "error: $*" >&2
  exit 2
}

cd "/host" || die "/host not found"

cabal v2-build --enable-executable-static "$@" || die "cabal: exit code $?"

PUID="$(stat -c '%u' /host)"
PGID="$(stat -c '%g' /host)"
chown -R "${PUID}:${PGID}" "dist-newstyle" || die "chown: exit code $?"

I am not going to put the whole Makefile command here, because it depends on other parts of the Makefile. (I hope to publish the Makefile article series soon!) The relevant part of the command is how the container is run.

docker run --rm -it -v "$(CURDIR):/host" "$(CABAL_STATIC_REPO)" \
  "/host/script/build-cabal-static.sh" --flags=static $(CABAL_ARGS)

This command runs a container with an interactive terminal so that it is automatically removed when it stops. The current directory is mounted at /host within the container. The CABAL_STATIC_REPO variable specifies the selected image. The above script is executed using the absolute path within the container. The --flags=static option is passed to the script, as well as any options configured by the Makefile in the CABAL_ARGS variable. This includes version-specific cabal.project configuration when necessary.

I tested with all four of the supported GHC versions, and everything builds without issue!